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 proxy from './proxy';
import routes from './routes'; import routes from './routes';
import { getBranchInfo } from './utils'; import { getBranchInfo } from './utils';
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const versionInfo = getBranchInfo(); const versionInfo = getBranchInfo();
process.env.VERSION = JSON.stringify(versionInfo); process.env.VERSION = JSON.stringify(versionInfo);
@@ -79,6 +78,9 @@ export default defineConfig({
model: {}, model: {},
initialState: {}, initialState: {},
request: {}, request: {},
routePrefetch: {
defaultPrefetch: 'intent'
},
keepalive: keepAlive, keepalive: keepAlive,
locale: { locale: {
antd: true, antd: true,
+7 -2
View File
@@ -79,10 +79,15 @@ export async function getInitialState(): Promise<{
const getAppVersionInfo = async () => { const getAppVersionInfo = async () => {
try { try {
const data = await queryVersionInfo(); 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, { setAtomStorage(GPUStackVersionAtom, {
...data, ...data,
isProduction isProd: !isDev && !isRc,
isDev,
isRc
}); });
} catch (error) { } catch (error) {
console.error('queryVersionInfo error', error); console.error('queryVersionInfo error', error);
+2
View File
@@ -29,6 +29,7 @@ export const regionOSImageListAtom = atom<
{ {
label: string; label: string;
value: string; value: string;
os_image: string;
name: string; name: string;
description: string; description: string;
vendor: string; vendor: string;
@@ -60,4 +61,5 @@ export const fromClusterCreationAtom = atom(false);
*/ */
export const clusterSessionAtom = atom<{ export const clusterSessionAtom = atom<{
firstAddWorker: boolean; firstAddWorker: boolean;
firstAddCluster: boolean;
} | null>(null); } | null>(null);
+6 -2
View File
@@ -6,11 +6,15 @@ export const userAtom = atomWithStorage<any>('userInfo', null);
export const GPUStackVersionAtom = atom<{ export const GPUStackVersionAtom = atom<{
version: string; version: string;
git_commit: string; git_commit: string;
isProduction: boolean; isProd: boolean;
isDev?: boolean;
isRc?: boolean;
}>({ }>({
version: '', version: '',
git_commit: '', git_commit: '',
isProduction: false isProd: false,
isDev: false,
isRc: false
}); });
export const UpdateCheckAtom = atom<{ export const UpdateCheckAtom = atom<{
+3 -2
View File
@@ -70,8 +70,9 @@ const AutoImage: React.FC<
setIsError(false); setIsError(false);
}, [props.onLoad]); }, [props.onLoad]);
const handleOnError = useCallback(() => { const handleOnError = useCallback((e: any) => {
setIsError(true); setIsError(true);
e.target.src = fallbackImg;
}, []); }, []);
useEffect(() => { 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; onChange: (value: string) => void;
onBlur?: (e: any) => void; onBlur?: (e: any) => void;
placeholder?: string; placeholder?: string;
trim?: boolean;
sourceOptions?: Global.HintOptions[]; sourceOptions?: Global.HintOptions[];
} }
const matchReg = /[^=]+=[^=]*$/; const matchReg = /[^=]+=[^=]*$/;
const HintInput: React.FC<HintInputProps> = (props) => { 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 cursorPosRef = React.useRef(0);
const contextBeforeCursorRef = React.useRef(''); const contextBeforeCursorRef = React.useRef('');
const [options, setOptions] = React.useState< const [options, setOptions] = React.useState<
@@ -70,11 +71,7 @@ const HintInput: React.FC<HintInputProps> = (props) => {
const handleInput = (e: any) => { const handleInput = (e: any) => {
getContextBeforeCursor(e); getContextBeforeCursor(e);
onChange(e.target.value?.trim()); onChange(e.target.value);
};
const handleOnChange = (value: string) => {
onChange(value?.trim());
}; };
const handleOnSelect = (value: string) => { const handleOnSelect = (value: string) => {
@@ -93,9 +90,10 @@ const HintInput: React.FC<HintInputProps> = (props) => {
onBlur={onBlur} onBlur={onBlur}
label={label} label={label}
options={options} options={options}
trim={trim}
style={{ width: '100%' }} 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 _ from 'lodash';
import React from 'react'; import React from 'react';
import Wrapper from '../label-selector/wrapper'; import Wrapper from '../label-selector/wrapper';
@@ -12,13 +11,13 @@ interface ListInputProps {
options?: Global.HintOptions[]; options?: Global.HintOptions[];
placeholder?: string; placeholder?: string;
labelExtra?: React.ReactNode; labelExtra?: React.ReactNode;
trim?: boolean;
onChange: (data: string[]) => void; onChange: (data: string[]) => void;
onBlur?: (e: any, index: number) => void; onBlur?: (e: any, index: number) => void;
onDelete?: (index: number) => void; onDelete?: (index: number) => void;
} }
const ListInput: React.FC<ListInputProps> = (props) => { const ListInput: React.FC<ListInputProps> = (props) => {
const intl = useIntl();
const { const {
dataList, dataList,
label, label,
@@ -28,7 +27,8 @@ const ListInput: React.FC<ListInputProps> = (props) => {
onDelete, onDelete,
btnText, btnText,
options, options,
labelExtra labelExtra,
trim = true
} = props; } = props;
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]); const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
const countRef = React.useRef(0); const countRef = React.useRef(0);
@@ -97,6 +97,7 @@ const ListInput: React.FC<ListInputProps> = (props) => {
onBlur={(e) => onBlur?.(e, index)} onBlur={(e) => onBlur?.(e, index)}
onRemove={() => handleOnRemove(index)} onRemove={() => handleOnRemove(index)}
onChange={(val) => handleOnChange(val, index)} onChange={(val) => handleOnChange(val, index)}
trim={trim}
/> />
); );
})} })}
+12 -2
View File
@@ -13,10 +13,19 @@ interface LabelItemProps {
label?: string; label?: string;
placeholder?: string; placeholder?: string;
options?: Global.HintOptions[]; options?: Global.HintOptions[];
trim?: boolean;
} }
const ListItem: React.FC<LabelItemProps> = (props) => { 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) => { const handleOnChange = (value: any) => {
onChange(value); onChange(value);
@@ -30,6 +39,7 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
onBlur={onBlur} onBlur={onBlur}
label={label} label={label}
sourceOptions={options} sourceOptions={options}
trim={trim}
placeholder={props.placeholder} placeholder={props.placeholder}
/> />
<Button <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( const handleOnWheel = useCallback(
(e: any) => { (e: any) => {
const scrollTop = scrollEventElement?.scrollTop; const scrollTop = scrollEventElement?.current.scrollTop;
const scrollHeight = scrollEventElement?.scrollHeight; const scrollHeight = scrollEventElement?.current.scrollHeight;
const clientHeight = scrollEventElement?.clientHeight; const clientHeight = scrollEventElement?.current.clientHeight;
stopScroll.current = scrollTop + clientHeight <= scrollHeight; stopScroll.current = scrollTop + clientHeight <= scrollHeight;
@@ -1,4 +1,5 @@
import useSetChunkFetch from '@/hooks/use-chunk-fetch'; import useSetChunkFetch from '@/hooks/use-chunk-fetch';
import { useMemoizedFn } from 'ahooks';
import { Spin } from 'antd'; import { Spin } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; 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 }) => { async (data: { isTop: boolean; isBottom: boolean }) => {
const { isTop, isBottom } = data; const { isTop, isBottom } = data;
setIsAtTop(isTop); setIsAtTop(isTop);
@@ -225,17 +226,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
} else if (isBottom && page < totalPage) { } else if (isBottom && page < totalPage) {
// getNextPage(); // getNextPage();
} }
}, }
[
loading,
logs.length,
pageSize,
enableScorllLoad,
page,
totalPage,
setScrollPos,
createChunkConnection
]
); );
const debouncedScroll = useCallback( const debouncedScroll = useCallback(
@@ -90,6 +90,14 @@ const SealAutoComplete: React.FC<
const handleOnSelect = (value: any, option: any) => { const handleOnSelect = (value: any, option: any) => {
onSelect?.(value, option); onSelect?.(value, option);
}; };
const handleOnInput = (e: any) => {
if (trim) {
e.target.value = e.target.value?.trim();
}
props.onInput?.(e);
};
const renderAfter = () => { const renderAfter = () => {
if (loading) { if (loading) {
return ( return (
@@ -123,6 +131,7 @@ const SealAutoComplete: React.FC<
> >
<AutoComplete <AutoComplete
{...rest} {...rest}
trim={trim}
ref={inputRef} ref={inputRef}
placeholder={ placeholder={
isFocus || !label ? ( isFocus || !label ? (
@@ -141,6 +150,7 @@ const SealAutoComplete: React.FC<
onSearch={handleSearch} onSearch={handleSearch}
onChange={handleChange} onChange={handleChange}
popupRender={popupRender} popupRender={popupRender}
onInput={handleOnInput}
></AutoComplete> ></AutoComplete>
</Wrapper> </Wrapper>
</SelectWrapper> </SelectWrapper>
+24 -8
View File
@@ -2,10 +2,20 @@ import IconFont from '@/components/icon-font';
import type { SelectProps } from 'antd'; import type { SelectProps } from 'antd';
import { Select } from 'antd'; import { Select } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import styled from 'styled-components';
import NotFoundContent from '../components/not-found-content'; import NotFoundContent from '../components/not-found-content';
const BaseSelect: React.FC<SelectProps & { ref?: any }> = forwardRef( const Footer = styled.div`
(props, ref) => { color: var(--ant-color-text-tertiary);
margin-top: 8px;
margin-bottom: 0;
padding: 8px 12px;
border-top: 1px solid var(--ant-color-split);
`;
const BaseSelect: React.FC<
SelectProps & { ref?: any; footer?: React.ReactNode }
> = forwardRef((props, ref) => {
const { notFoundContent, loading, ...restProps } = props; const { notFoundContent, loading, ...restProps } = props;
const [isFocus, setIsFocus] = React.useState(false); const [isFocus, setIsFocus] = React.useState(false);
const inputRef = React.useRef<any>(null); const inputRef = React.useRef<any>(null);
@@ -36,18 +46,24 @@ const BaseSelect: React.FC<SelectProps & { ref?: any }> = forwardRef(
<Select <Select
{...restProps} {...restProps}
notFoundContent={ notFoundContent={
<NotFoundContent <NotFoundContent loading={loading} notFoundContent={notFoundContent} />
loading={loading}
notFoundContent={notFoundContent}
/>
} }
ref={inputRef} ref={inputRef}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
suffixIcon={renderSuffixIcon()} suffixIcon={renderSuffixIcon()}
popupRender={
props.footer
? (originNode) => (
<>
{originNode}
{props.footer && <Footer>{props.footer}</Footer>}
</>
)
: undefined
}
/> />
); );
} });
);
export default BaseSelect; export default BaseSelect;
@@ -12,10 +12,11 @@
.note-info { .note-info {
margin-left: 4px; margin-left: 4px;
display: flex;
align-items: center;
} }
.star { .star {
position: relative; font-size: 0.7rem;
top: 2px;
} }
} }
@@ -14,7 +14,7 @@ const NoteInfo: React.FC<NoteInfoProps> = (props) => {
if (!label) return null; if (!label) return null;
const renderRequiredStar = required ? ( const renderRequiredStar = required ? (
<span className="star" style={{ color: 'red' }}> <span className="star" style={{ color: 'red' }}>
*
</span> </span>
) : null; ) : null;
+1
View File
@@ -64,6 +64,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
required={required} required={required}
description={description} description={description}
disabled={props.disabled} disabled={props.disabled}
labelExtra={props.labelExtra}
hasPrefix={!!props.prefix} hasPrefix={!!props.prefix}
onClick={handleClickWrapper} onClick={handleClickWrapper}
> >
+3 -1
View File
@@ -10,7 +10,9 @@ import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select'; import SelectWrapper from './wrapper/select';
const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => { const SealSelect: React.FC<
SelectProps & SealFormItemProps & { footer?: React.ReactNode }
> = (props) => {
const { const {
label, label,
placeholder, placeholder,
+1 -2
View File
@@ -1,7 +1,7 @@
import breakpoints from '@/config/breakpoints'; import breakpoints from '@/config/breakpoints';
import InfiniteScroller from '@/pages/_components/infinite-scroller'; import InfiniteScroller from '@/pages/_components/infinite-scroller';
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context'; 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 _ from 'lodash';
import ResizeObserver from 'rc-resize-observer'; import ResizeObserver from 'rc-resize-observer';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
@@ -122,7 +122,6 @@ const CardList: React.FC<CatalogListProps> = (props) => {
/> />
</InfiniteScroller> </InfiniteScroller>
</ResizeObserver> </ResizeObserver>
<FloatButton.BackTop visibilityHeight={1000} />
</div> </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 Logo from '@/assets/images/gpustack-logo.png';
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user'; import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import { getAtomStorage } from '@/atoms/utils';
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import { Button } from 'antd'; import { Button } from 'antd';
import { useAtom } from 'jotai';
import React from 'react'; import React from 'react';
import './index.less'; import './index.less';
const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => { const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
const latestVersion = getAtomStorage(UpdateCheckAtom).latest_version; const [gpuStackVersionAtom] = useAtom(GPUStackVersionAtom);
const currentVersion = getAtomStorage(GPUStackVersionAtom)?.version; const [userDataAtom] = useAtom(userAtom);
const [updateCheck] = useAtom(UpdateCheckAtom);
const isProd = // current version info
currentVersion?.indexOf('rc') === -1 && const {
currentVersion?.indexOf('0.0.0') === -1; 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'); const uiVersion = document.documentElement.getAttribute('data-version');
@@ -29,10 +40,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
</span> </span>
)} )}
{isProd ? ( {isProd ? (
<span className="val"> <span className="val">{currentVersion || git_commit}</span>
{getAtomStorage(GPUStackVersionAtom)?.version ||
getAtomStorage(GPUStackVersionAtom)?.git_commit}
</span>
) : ( ) : (
<span className="val dev"> <span className="val dev">
<span className="item"> <span className="item">
@@ -40,9 +48,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
{' '} {' '}
{intl.formatMessage({ id: 'common.footer.version.server' })} {intl.formatMessage({ id: 'common.footer.version.server' })}
</span> </span>
{currentVersion.indexOf('0.0.0') > -1 {isDev ? git_commit : currentVersion}
? getAtomStorage(GPUStackVersionAtom)?.git_commit
: getAtomStorage(GPUStackVersionAtom)?.version}
</span> </span>
<span className="item"> <span className="item">
<span className="tl">UI</span> <span className="tl">UI</span>
@@ -51,12 +57,10 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
</span> </span>
)} )}
</div> </div>
{getAtomStorage(userAtom)?.is_admin && isProd && ( {is_admin && isProd && (
<div className="upgrade-text"> <div className="upgrade-text">
<span className="m-l-5"> <span className="m-l-5">
{latestVersion && {latestVersion && latestVersion !== currentVersion && !isDev
latestVersion !== currentVersion &&
latestVersion.indexOf('0.0.0') === -1
? intl.formatMessage( ? intl.formatMessage(
{ id: 'users.version.update' }, { id: 'users.version.update' },
{ version: latestVersion } { version: latestVersion }
+1 -1
View File
@@ -23,7 +23,7 @@ declare namespace Global {
require_password_change: boolean; require_password_change: boolean;
id: number; id: number;
source: string; source: string;
avatar: string; avatar_url: string;
} }
type EmptyObject = Record<never, never>; type EmptyObject = Record<never, never>;
+1 -1
View File
@@ -2,7 +2,7 @@ export const HEADER_HEIGHT = 56;
export const DEFAULT_ENTER_PAGE = { export const DEFAULT_ENTER_PAGE = {
adminForNormal: '/dashboard', adminForNormal: '/dashboard',
adminForFirst: '/models/deployments', adminForFirst: '/resources/workers',
user: '/models/user-models', user: '/models/user-models',
login: '/login' login: '/login'
}; };
+4 -4
View File
@@ -122,12 +122,12 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
initialState?.currentUser?.is_admin && initialState?.currentUser?.is_admin &&
updateCheck.latest_version && updateCheck.latest_version &&
updateCheck.latest_version !== version?.version && updateCheck.latest_version !== version?.version &&
updateCheck.latest_version?.indexOf('0.0.0') === -1 && version?.isProd
updateCheck.latest_version?.indexOf('rc') === -1
); );
}, [ }, [
updateCheck.latest_version, updateCheck.latest_version,
version.version, version.version,
version.isProd,
initialState?.currentUser?.is_admin initialState?.currentUser?.is_admin
]); ]);
@@ -272,7 +272,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
<Avatar <Avatar
size={24} size={24}
style={{ ...avatarStyle }} style={{ ...avatarStyle }}
src={initialState?.currentUser?.avatar} src={initialState?.currentUser?.avatar_url}
icon={ icon={
<IconFont type="icon-user-filled" className="font-size-24" /> <IconFont type="icon-user-filled" className="font-size-24" />
} }
@@ -322,7 +322,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
<Avatar <Avatar
size={24} size={24}
style={{ ...avatarStyle }} style={{ ...avatarStyle }}
src={initialState?.currentUser?.avatar} src={initialState?.currentUser?.avatar_url}
icon={<IconFont type="icon-user-filled" className="font-size-24" />} icon={<IconFont type="icon-user-filled" className="font-size-24" />}
/> />
</IconWrapper> </IconWrapper>
+18 -221
View File
@@ -1,16 +1,11 @@
// @ts-nocheck // @ts-nocheck
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache'; 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 DarkMask from '@/components/dark-mask';
import IconFont from '@/components/icon-font'; 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 routeCachekey from '@/config/route-cachekey';
import { DEFAULT_ENTER_PAGE } from '@/config/settings'; import { DEFAULT_ENTER_PAGE } from '@/config/settings';
import useBodyScroll from '@/hooks/use-body-scroll';
import useOverlayScroller from '@/hooks/use-overlay-scroller'; import useOverlayScroller from '@/hooks/use-overlay-scroller';
import useUserSettings from '@/hooks/use-user-settings'; import useUserSettings from '@/hooks/use-user-settings';
import { logout } from '@/pages/login/apis'; import { logout } from '@/pages/login/apis';
@@ -18,7 +13,6 @@ import { useAccessMarkedRoutes } from '@@/plugin-access';
import { useModel } from '@@/plugin-model'; import { useModel } from '@@/plugin-model';
import { ProLayout } from '@ant-design/pro-components'; import { ProLayout } from '@ant-design/pro-components';
import { import {
Link,
Outlet, Outlet,
dropByCacheKey, dropByCacheKey,
history, history,
@@ -29,18 +23,17 @@ import {
useNavigate, useNavigate,
type IRoute type IRoute
} from '@umijs/max'; } 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 'driver.js/dist/driver.css';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo } from 'react';
import { PageContainerInner } from '../pages/_components/page-box'; import { PageContainerInner } from '../pages/_components/page-box';
import Exception from './Exception'; import Exception from './Exception';
import './Layout.css'; import './Layout.css';
import { LogoIcon, SLogoIcon } from './Logo'; import { LogoIcon, SLogoIcon } from './Logo';
import ErrorBoundary from './error-boundary'; import ErrorBoundary from './error-boundary';
import { ExtraContent } from './extraRender'; import { ExtraContent } from './extraRender';
import { getRightRenderContent } from './rightRender';
import { patchRoutes } from './runtime'; import { patchRoutes } from './runtime';
import SiderMenu from './sider-menu'; import SiderMenu from './sider-menu';
@@ -57,16 +50,11 @@ const NO_CONTAINER_PAGES = [
const loginPath = DEFAULT_ENTER_PAGE.login; 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 // Filter out the routes that need to be displayed, where filterFn indicates the levels that should not be shown
const filterRoutes = ( const filterRoutes = (
routes: IRoute[], routes: IRoute[],
filterFn: (route: IRoute) => boolean filterFn: (route: IRoute) => boolean
) => { ): any[] => {
if (routes.length === 0) { if (routes.length === 0) {
return []; return [];
} }
@@ -117,21 +105,13 @@ export default (props: any) => {
defer: false defer: false
}); });
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { themeData, setTheme, setUserSettings, userSettings, isDarkTheme } = const { themeData, setUserSettings, userSettings } = useUserSettings();
useUserSettings();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const { initialize: initializeMenu } = useOverlayScroller();
const [userInfo] = useAtom(userAtom); const [userInfo] = useAtom(userAtom);
const [routeCache] = useAtom(routeCacheAtom); const [routeCache] = useAtom(routeCacheAtom);
const [version] = useAtom(GPUStackVersionAtom);
const [updateCheck] = useAtom(UpdateCheckAtom);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const intl = useIntl(); const intl = useIntl();
const { clientRoutes, pluginManager } = useAppData(); const { clientRoutes } = useAppData();
// const [collapsed, setCollapsed] = useState(userSettings.collapsed || false);
const [collapseValue, setCollapseValue] = useState(false);
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
const initialInfo = (useModel && useModel('@@initialState')) || { const initialInfo = (useModel && useModel('@@initialState')) || {
initialState: undefined, initialState: undefined,
@@ -150,23 +130,6 @@ export default (props: any) => {
return intl.formatMessage({ id: args.id }); 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) => { const initRouteCacheValue = (pathname) => {
if (routeCache.get(pathname) === undefined && routeCachekey[pathname]) { if (routeCache.get(pathname) === undefined && routeCachekey[pathname]) {
setRouteCache(pathname, false); setRouteCache(pathname, false);
@@ -184,22 +147,17 @@ export default (props: any) => {
const runtimeConfig = { const runtimeConfig = {
...initialInfo, ...initialInfo,
logout: async (userInfo) => { logout: async () => {
await logout(); await logout();
navigate(loginPath); navigate(loginPath);
}, },
showVersion: () => { showVersion: () => {},
return showVersion(); showShortcuts: () => {},
},
showShortcuts: () => {
return showShortcuts();
},
notFound: <span>404 not found</span> notFound: <span>404 not found</span>
}; };
const handleToggleCollapse = (e: any) => { const handleToggleCollapse = (e: any) => {
e.stopPropagation(); e.stopPropagation();
// setCollapsed(!collapsed);
setUserSettings({ setUserSettings({
...userSettings, ...userSettings,
collapsed: !userSettings.collapsed collapsed: !userSettings.collapsed
@@ -235,38 +193,6 @@ export default (props: any) => {
console.log('matchedRoute=========', matchedRoute, route); 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(() => { useEffect(() => {
const body = document.querySelector('body'); const body = document.querySelector('body');
if (body) { if (body) {
@@ -306,92 +232,13 @@ export default (props: any) => {
); );
}; };
const handleToggleGroup = (menuItemProps, e) => { const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
e.stopPropagation();
if (collapseKeys.has(menuItemProps.key)) {
collapseKeys.delete(menuItemProps.key);
} else {
collapseKeys.add(menuItemProps.key);
}
setCollapseKeys(new Set(collapseKeys));
};
const menuContentRender = (menuProps, defaultDom) => {
return <SiderMenu {...menuProps}></SiderMenu>; return <SiderMenu {...menuProps}></SiderMenu>;
}; };
const actionRender = (layoutProps) => { const onPageChange = (route: any) => {
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 { location } = history; const { location } = history;
const { pathname } = location; const { pathname } = location;
console.log('onPageChange', pathname, route);
initRouteCacheValue(pathname); initRouteCacheValue(pathname);
dropRouteCache(pathname); dropRouteCache(pathname);
@@ -413,7 +260,7 @@ export default (props: any) => {
} }
}; };
const onMenuHeaderClick = (e) => { const onMenuHeaderClick = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
const pagepath = initialState?.currentUser?.is_admin const pagepath = initialState?.currentUser?.is_admin
@@ -423,65 +270,13 @@ export default (props: any) => {
navigate(pagepath); navigate(pagepath);
}; };
const onCollapse = (value) => { const onCollapse = (value: boolean) => {
setUserSettings({ setUserSettings({
...userSettings, ...userSettings,
collapsed: value 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 ( return (
<ConfigProvider <ConfigProvider
componentSize="large" componentSize="large"
@@ -530,7 +325,7 @@ export default (props: any) => {
type: 'group' type: 'group'
}} }}
splitMenus={true} splitMenus={true}
logo={collapsed ? SLogoIcon : LogoIcon} logo={userSettings.collapsed ? <SLogoIcon /> : <LogoIcon />}
menuContentRender={menuContentRender} menuContentRender={menuContentRender}
disableContentMargin disableContentMargin
{...runtimeConfig} {...runtimeConfig}
@@ -544,9 +339,11 @@ export default (props: any) => {
noAccessible={runtimeConfig?.noAccessible} noAccessible={runtimeConfig?.noAccessible}
> >
{isNoContainerPage ? ( {isNoContainerPage ? (
outlet <Outlet />
) : ( ) : (
<PageContainerInner>{outlet}</PageContainerInner> <PageContainerInner>
<Outlet />
</PageContainerInner>
)} )}
</Exception> </Exception>
</ProLayout> </ProLayout>
+1
View File
@@ -164,6 +164,7 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
key={key} key={key}
> >
<Link <Link
prefetch="intent"
to={menuItem.path.replace('/*', '')} to={menuItem.path.replace('/*', '')}
target={menuItem.target} target={menuItem.target}
className={cx(styles.menuItemWrapper, 'menu-item', { 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.autoDetect': 'Auto-detect',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)', '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': 'models.form.ollamalink':
'Find More in <a href="https://www.ollama.com/library" target="_blank">Ollama Library</a>.', 'Find More in <a href="https://www.ollama.com/library" target="_blank">Ollama Library</a>.',
'models.form.backend_parameters.llamabox.placeholder': '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': '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': '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': 'models.form.backend_parameters.vllm.tips':
'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.', 'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
'models.logs.pagination.prev': 'Previous {lines} Lines', 'models.logs.pagination.prev': 'Previous {lines} Lines',
@@ -260,5 +260,13 @@ export default {
'models.form.generic_proxy.button': 'Generic Proxy', 'models.form.generic_proxy.button': 'Generic Proxy',
'models.accessControlModal.includeusers': 'Include Users', 'models.accessControlModal.includeusers': 'Include Users',
'models.table.genericProxy': '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.keys.nofound': 'No matching API keys found.',
'noresult.catalog.title': 'No Models', 'noresult.catalog.title': 'No Models',
'noresult.catalog.subTitle': 'No models have been configured yet.', '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': 'playground.image.generate.error':
'Something went wrong. The image could not be generated.', 'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError': '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.autoDetect': 'Auto-detect',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)', '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) ========== // ========== 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', // 65. 'clusters.addworker.notSpecified': 'Not Specified',
// 66. 'clusters.addworker.autoDetect': 'Auto-detect', // 66. 'clusters.addworker.autoDetect': 'Auto-detect',
// 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)' // 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 ========== // ========== End of To-Do List ==========
+18 -5
View File
@@ -70,11 +70,11 @@ export default {
'models.form.ollamalink': 'models.form.ollamalink':
'<a href="https://www.ollama.com/library" target="_blank">Ollamaライブラリ</a>でさらに探す', '<a href="https://www.ollama.com/library" target="_blank">Ollamaライブラリ</a>でさらに探す',
'models.form.backend_parameters.llamabox.placeholder': 'models.form.backend_parameters.llamabox.placeholder':
'例: --ctx-size=8192=で名前と値を分ける)', '例: --ctx-size=8192=または空白で名前と値を分ける)',
'models.form.backend_parameters.vllm.placeholder': 'models.form.backend_parameters.vllm.placeholder':
'例: --max-model-len=8192=で名前と値を分ける)', '例: --max-model-len=8192=または空白で名前と値を分ける)',
'models.form.backend_parameters.sglang.placeholder': 'models.form.backend_parameters.sglang.placeholder':
'例: --context-length=8192=で名前と値を分ける)', '例: --context-length=8192=または空白で名前と値を分ける)',
'models.form.backend_parameters.vllm.tips': 'models.form.backend_parameters.vllm.tips':
'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.', 'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
'models.logs.pagination.prev': '前の{lines}行', 'models.logs.pagination.prev': '前の{lines}行',
@@ -260,7 +260,15 @@ export default {
'models.form.generic_proxy.button': 'Generic Proxy', 'models.form.generic_proxy.button': 'Generic Proxy',
'models.accessControlModal.includeusers': 'Include Users', 'models.accessControlModal.includeusers': 'Include Users',
'models.table.genericProxy': '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) ========== // ========== 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.', // 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.`, // 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.', // 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 ========== // ========== End of To-Do List ==========
+7 -1
View File
@@ -33,5 +33,11 @@ export default {
'noresult.keys.nofound': 'No matching API keys found.', 'noresult.keys.nofound': 'No matching API keys found.',
'noresult.catalog.title': 'No Models', 'noresult.catalog.title': 'No Models',
'noresult.catalog.subTitle': 'No models have been configured yet.', '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': 'playground.image.generate.error':
'Something went wrong. The image could not be generated.', 'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError': '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.rerank.query.validate': 'The query is required.' // 1. 'playground.rerank.query.validate': 'The query is required.'
// 2. 'playground.image.generate.error': 'Something went wrong. The image could not be generated.', // 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}.' // 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 ========== // ========== End of To-Do List ==========
+27 -43
View File
@@ -48,9 +48,9 @@ export default {
'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.', 'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.',
'cluster.provider.comingsoon': 'Скоро будет', 'cluster.provider.comingsoon': 'Скоро будет',
'clusters.addworker.nvidiaNotes-01': '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': 'clusters.addworker.nvidiaNotes-02':
'If a model directory already exists on the worker, you can specify the path to mount it.', 'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.',
'clusters.addworker.hygonNotes': '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>.', 'Если директория <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': 'clusters.addworker.corexNotes':
@@ -60,49 +60,33 @@ export default {
'clusters.addworker.cambriconNotes': '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>.', 'Если директория <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': '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>.', 'Если не удается обнаружить устройства, попробуйте удалить <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
'clusters.addworker.selectCluster': 'Select Cluster', 'clusters.addworker.selectCluster': 'Выбрать кластер',
'clusters.addworker.selectCluster.tips': 'clusters.addworker.selectCluster.tips':
'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.', 'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
'clusters.addworker.selectGPU': 'Select GPU Vendor', 'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
'clusters.addworker.checkEnv': 'Check Environment', 'clusters.addworker.checkEnv': 'Проверить окружение',
'clusters.addworker.specifyArgs': 'Specify Arguments', 'clusters.addworker.specifyArgs': 'Указать аргументы',
'clusters.addworker.runCommand': 'Run Command', 'clusters.addworker.runCommand': 'Выполнить команду',
'clusters.addworker.specifyWorkerIP': 'Specify Worker IP', 'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP', 'clusters.addworker.detectWorkerIP': 'Автоопределение IP воркера',
'clusters.addworker.enterWorkerIP': 'Enter worker IP', 'clusters.addworker.enterWorkerIP': 'Введите IP воркера',
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.', 'clusters.addworker.enterWorkerIP.error': 'Пожалуйста, введите IP воркера.',
'clusters.addworker.extraVolume': 'Additional Volume Mount', 'clusters.addworker.extraVolume': 'Дополнительное монтирование тома',
'clusters.addworker.configSummary': 'Configuration Summary', 'clusters.addworker.configSummary': 'Сводка конфигурации',
'clusters.addworker.gpuVendor': 'GPU Vendor', 'clusters.addworker.gpuVendor': 'Производитель GPU',
'clusters.addworker.workerIP': 'Worker IP', 'clusters.addworker.workerIP': 'IP воркера',
'clusters.addworker.notSpecified': 'Not Specified', 'clusters.addworker.notSpecified': 'Не указано',
'clusters.addworker.autoDetect': 'Auto-detect', 'clusters.addworker.autoDetect': 'Автоопределение',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)', 'напр. /data/models (путь должен начинаться с /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device' '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) ========== // ========== 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>.', // 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.addworker.selectCluster': 'Select Cluster', // 2. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
// 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 ==========
+2 -2
View File
@@ -31,9 +31,9 @@ export default {
'dashboard.usage.datePicker.last7days': 'Последние 7 Days', 'dashboard.usage.datePicker.last7days': 'Последние 7 Days',
'dashboard.usage.datePicker.last30days': 'Последние 30 Days', 'dashboard.usage.datePicker.last30days': 'Последние 30 Days',
'dashboard.usage.datePicker.last60days': 'Последние 60 Days', 'dashboard.usage.datePicker.last60days': 'Последние 60 Days',
'dashboard.clusters': 'Clusters' 'dashboard.clusters': 'Кластеры'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'dashboard.clusters': 'Clusters',
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+38 -36
View File
@@ -2,7 +2,7 @@ export default {
'models.button.deploy': 'Развернуть модель', 'models.button.deploy': 'Развернуть модель',
'models.title': 'Модели', 'models.title': 'Модели',
'models.title.edit': 'Редактировать модель', 'models.title.edit': 'Редактировать модель',
'models.table.models': 'модели', 'models.table.models': 'Модели',
'models.table.name': 'Название модели', 'models.table.name': 'Название модели',
'models.form.source': 'Источник', 'models.form.source': 'Источник',
'models.form.repoid': 'ID репозитория', 'models.form.repoid': 'ID репозитория',
@@ -13,8 +13,10 @@ export default {
'models.form.env': 'Переменные окружения', 'models.form.env': 'Переменные окружения',
'models.form.configurations': 'Конфигурации', 'models.form.configurations': 'Конфигурации',
'models.form.s3address': 'S3-адрес', '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.partialoffload.tips':
'models.form.distribution.tips': `Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.`, 'При включении CPU оффлоудинга GPUStack будет выделять оперативную память, если ресурсов GPU недостаточно. Вы должны правильно настроить бэкенд вывода для использования гибридного CPU+GPU или полного CPU вывода.',
'models.form.distribution.tips':
'Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.',
'models.openinplayground': 'Открыть в Песочнице', 'models.openinplayground': 'Открыть в Песочнице',
'models.instances': 'инстансы', 'models.instances': 'инстансы',
'models.table.replicas.edit': 'Редактировать реплики', 'models.table.replicas.edit': 'Редактировать реплики',
@@ -69,13 +71,13 @@ export default {
'models.form.ollamalink': 'models.form.ollamalink':
'Больше моделей в библиотеке <a href="https://www.ollama.com/library" target="_blank">Ollama</a>', 'Больше моделей в библиотеке <a href="https://www.ollama.com/library" target="_blank">Ollama</a>',
'models.form.backend_parameters.llamabox.placeholder': 'models.form.backend_parameters.llamabox.placeholder':
'например: --ctx-size=8192(параметр и значение разделены знаком =)', 'например: --ctx-size=8192(параметр и значение разделены знаком = или пробелом',
'models.form.backend_parameters.vllm.placeholder': 'models.form.backend_parameters.vllm.placeholder':
'например: --max-model-len=8192(параметр и значение разделены знаком =)', 'например: --max-model-len=8192(параметр и значение разделены знаком = или пробелом',
'models.form.backend_parameters.sglang.placeholder': 'models.form.backend_parameters.sglang.placeholder':
'например: --context-length=8192(параметр и значение разделены знаком =)', 'например: --context-length=8192(параметр и значение разделены знаком = или пробелом',
'models.form.backend_parameters.vllm.tips': '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.prev': 'Предыдущие {lines} строк',
'models.logs.pagination.next': 'Следующие {lines} строк', 'models.logs.pagination.next': 'Следующие {lines} строк',
'models.logs.pagination.last': 'Последняя страница', 'models.logs.pagination.last': 'Последняя страница',
@@ -89,11 +91,11 @@ export default {
'models.form.backend.llamabox': 'models.form.backend.llamabox':
'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.', 'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
'models.form.backend.vllm': 'models.form.backend.vllm':
'Built-in support for NVIDIA, AMD, Ascend, Hygon, Iluvatar, and MetaX devices.', 'Встроенная поддержка устройств NVIDIA, AMD, Ascend, Hygon, Iluvatar и MetaX.',
'models.form.backend.voxbox': 'Only supports NVIDIA GPUs and CPUs.', 'models.form.backend.voxbox': 'Поддерживает только GPU NVIDIA и CPU.',
'models.form.backend.mindie': 'Only supports Ascend NPUs.', 'models.form.backend.mindie': 'Поддерживает только Ascend NPU.',
'models.form.backend.sglang': 'models.form.backend.sglang':
'Built-in support for NVIDIA/AMD GPUs and Ascend NPUs.', 'Встроенная поддержка GPU NVIDIA/AMD и Ascend NPU.',
'models.form.search.gguftips': 'models.form.search.gguftips':
'Для воркеров на macOS/Windows отметьте GGUF (для аудиомоделей снимите).', 'Для воркеров на macOS/Windows отметьте GGUF (для аудиомоделей снимите).',
'models.form.button.addlabel': 'Добавить метку', 'models.form.button.addlabel': 'Добавить метку',
@@ -120,9 +122,9 @@ export default {
'models.form.moreparameters': 'Описание параметров', 'models.form.moreparameters': 'Описание параметров',
'models.table.vram.allocated': 'Выделенная VRAM', 'models.table.vram.allocated': 'Выделенная VRAM',
'models.form.backend.warning': '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': 'models.form.backend.warning.gguf':
'Please ensure that the selected custom backend supports GGUF models.', 'Пожалуйста, убедитесь, что выбранный пользовательский бэкенд поддерживает модели GGUF.',
'models.form.ollama.warning': 'models.form.ollama.warning':
'Чтобы развернуть бэкенд для моделей Ollama с использованием llama-box , выполните следующие шаги.', 'Чтобы развернуть бэкенд для моделей Ollama с использованием llama-box , выполните следующие шаги.',
'models.form.backend.warning.llamabox': 'models.form.backend.warning.llamabox':
@@ -147,7 +149,8 @@ export default {
'Изменения вступят в силу только после удаления и повторного создания инстанса.', 'Изменения вступят в силу только после удаления и повторного создания инстанса.',
'models.table.download.progress': 'Прогресс', 'models.table.download.progress': 'Прогресс',
'models.table.button.apiAccessInfo': 'Доступ к API', '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.endpoint': 'URL доступа',
'models.table.apiAccessInfo.modelName': 'Имя модели', 'models.table.apiAccessInfo.modelName': 'Имя модели',
'models.table.apiAccessInfo.apikey': 'Ключ API', 'models.table.apiAccessInfo.apikey': 'Ключ API',
@@ -165,7 +168,8 @@ export default {
'<span class="bold-text">После обновления до версии (v0.7.0),</span> все ранее развёрнутые модели продолжат работать в обычном режиме.', '<span class="bold-text">После обновления до версии (v0.7.0),</span> все ранее развёрнутые модели продолжат работать в обычном режиме.',
'models.ollama.deprecated.issue': 'models.ollama.deprecated.issue':
'См. связанную проблему: <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">#1979 on GitHub</a>.', 'См. связанную проблему: <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': 'models.backend.mindie.310p':
'Ascend 310P поддерживает только FP16, поэтому необходимо установить --dtype=float16.', 'Ascend 310P поддерживает только FP16, поэтому необходимо установить --dtype=float16.',
'models.form.gpuCount': 'GPU на реплику', 'models.form.gpuCount': 'GPU на реплику',
@@ -181,9 +185,9 @@ export default {
'models.table.accessScope.all': 'Все пользователи', 'models.table.accessScope.all': 'Все пользователи',
'models.table.userSelection': 'Выбор пользователей', 'models.table.userSelection': 'Выбор пользователей',
'models.button.accessSettings.tips': 'models.button.accessSettings.tips':
'Changes to access settings take effect after one minute.', 'Изменения в настройках доступа вступают в силу через одну минуту.',
'models.table.userSelection.tips': 'models.table.userSelection.tips':
'Admin users can access all models by default.', 'Администраторы по умолчанию имеют доступ ко всем моделям.',
'models.table.filterByName': 'Фильтр по имени пользователя', 'models.table.filterByName': 'Фильтр по имени пользователя',
'models.table.admin': 'Администратор', 'models.table.admin': 'Администратор',
'models.table.noselected': 'Пользователи не выбраны', 'models.table.noselected': 'Пользователи не выбраны',
@@ -197,7 +201,7 @@ export default {
'models.form.maxCPUSize': 'Максимальный размер CPU кэша (ГиБ)', 'models.form.maxCPUSize': 'Максимальный размер CPU кэша (ГиБ)',
'models.form.remoteURL': 'URL удаленного хранилища', 'models.form.remoteURL': 'URL удаленного хранилища',
'models.form.remoteURL.tips': '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': 'models.form.runCommandPlaceholder':
'напр., vllm serve Qwen/Qwen2.5-1.5B-Instruct', 'напр., vllm serve Qwen/Qwen2.5-1.5B-Instruct',
'models.accessSettings.public': 'Публичный', 'models.accessSettings.public': 'Публичный',
@@ -212,7 +216,7 @@ export default {
'models.form.gpusAllocationType.auto': 'Авто', 'models.form.gpusAllocationType.auto': 'Авто',
'models.form.gpusAllocationType.custom': 'Вручную', 'models.form.gpusAllocationType.custom': 'Вручную',
'models.form.gpusAllocationType.auto.tips': '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': 'models.form.gpusAllocationType.custom.tips':
'Вы можете указать точное количество GPU на реплику.', 'Вы можете указать точное количество GPU на реплику.',
'models.mymodels.status.inactive': 'Остановлен', 'models.mymodels.status.inactive': 'Остановлен',
@@ -252,31 +256,29 @@ export default {
'models.form.backend.custom': 'Пользовательский', 'models.form.backend.custom': 'Пользовательский',
'models.form.rules.name': 'models.form.rules.name':
'До 63 символов; только буквы, цифры, точки (.), подчёркивания (_) и дефисы (-); должно начинаться и заканчиваться буквенно-цифровым символом.', 'До 63 символов; только буквы, цифры, точки (.), подчёркивания (_) и дефисы (-); должно начинаться и заканчиваться буквенно-цифровым символом.',
'models.catalog.button.explore': 'Explore More Models', 'models.catalog.button.explore': 'Исследовать больше моделей',
'models.catalog.precision': 'Точность', 'models.catalog.precision': 'Точность',
'models.form.gpuPerReplica.tips': 'Введите произвольное число', 'models.form.gpuPerReplica.tips': 'Введите произвольное число',
'models.form.generic_proxy': 'Включить универсальный прокси', 'models.form.generic_proxy': 'Включить универсальный прокси',
'models.form.generic_proxy.tips': '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.form.generic_proxy.button': 'Универсальный прокси',
'models.accessControlModal.includeusers': 'Включить пользователей', 'models.accessControlModal.includeusers': 'Включить пользователей',
'models.table.genericProxy': '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.catalog.button.explore': 'Explore More Models', // 1. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
// 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.', // 2. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
// 3. 'models.form.backend.vllm': 'Built-in support for NVIDIA, AMD, Ascend, Hygon, Iluvatar, and MetaX devices.', // 3. 'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.',
// 4. 'models.form.backend.voxbox': 'Only supports NVIDIA GPUs and CPUs.', // 4. 'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`
// 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.',
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+18 -3
View File
@@ -1,10 +1,12 @@
export default { export default {
'noresult.button.add': 'Добавить сейчас', 'noresult.button.add': 'Добавить сейчас',
'noresult.mymodels.title': 'Нет доступных моделей', 'noresult.mymodels.title': 'Нет доступных моделей',
'noresult.mymodels.subTitle': 'Обратитесь к администратору для получения доступа.', 'noresult.mymodels.subTitle':
'Обратитесь к администратору для получения доступа.',
'noresult.mymodels.nofound': 'Подходящие модели не найдены', 'noresult.mymodels.nofound': 'Подходящие модели не найдены',
'noresult.deployments.title': 'Нет развернутых моделей', 'noresult.deployments.title': 'Нет развернутых моделей',
'noresult.deployments.subTitle': 'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.', 'noresult.deployments.subTitle':
'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.',
'noresult.gpus.title': 'GPU устройства не обнаружены', 'noresult.gpus.title': 'GPU устройства не обнаружены',
'noresult.gpus.subTitle': 'Проверьте, что статус Worker READY.', 'noresult.gpus.subTitle': 'Проверьте, что статус Worker READY.',
'noresult.gpus.nofound': 'Подходящие GPU устройства не найдены.', 'noresult.gpus.nofound': 'Подходящие GPU устройства не найдены.',
@@ -32,5 +34,18 @@ export default {
'noresult.keys.nofound': 'Подходящие API-ключи не найдены.', 'noresult.keys.nofound': 'Подходящие API-ключи не найдены.',
'noresult.catalog.title': 'Нет моделей', 'noresult.catalog.title': 'Нет моделей',
'noresult.catalog.subTitle': 'Модели еще не настроены.', '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.model.noavailable.tips2':
'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.', 'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.',
'playground.rerank.query.validate': 'Необходимо указать запрос.', 'playground.rerank.query.validate': 'Необходимо указать запрос.',
'playground.image.generate.error': 'Произошла ошибка. Не удалось сгенерировать изображение.', 'playground.image.generate.error':
'playground.uploadfile.sizeError': 'Размер файла превышает ограничение. Максимальный размер: {size}.' 'Произошла ошибка. Не удалось сгенерировать изображение.',
'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) ========== // ========== 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 ========== // ========== End of To-Do List ==========
+5 -1
View File
@@ -78,5 +78,9 @@ export default {
'clusters.addworker.autoDetect': '自动检测', 'clusters.addworker.autoDetect': '自动检测',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'例如:/data/models(路径需以 / 开头)', '例如:/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': 'models.form.ollamalink':
'在 <a href="https://www.ollama.com/library" target="_blank">Ollama Library</a> 中查找', '在 <a href="https://www.ollama.com/library" target="_blank">Ollama Library</a> 中查找',
'models.form.backend_parameters.llamabox.placeholder': 'models.form.backend_parameters.llamabox.placeholder':
'例如,--ctx-size=8192(参数名和值用 = 号分隔)', '例如,--ctx-size=8192(参数名和值用 = 号或空格分隔)',
'models.form.backend_parameters.vllm.placeholder': 'models.form.backend_parameters.vllm.placeholder':
'例如,--max-model-len=8192(参数名和值用 = 号分隔)', '例如,--max-model-len=8192(参数名和值用 = 号或空格分隔)',
'models.form.backend_parameters.sglang.placeholder': 'models.form.backend_parameters.sglang.placeholder':
'例如,--context-length=8192(参数名和值用 = 号分隔)', '例如,--context-length=8192(参数名和值用 = 号或空格分隔)',
'models.form.backend_parameters.vllm.tips': 'models.form.backend_parameters.vllm.tips':
'更多 {backend} 参数说明查看<a href={link} target="_blank">这里</a>。', '更多 {backend} 参数说明查看<a href={link} target="_blank">这里</a>。',
'models.logs.pagination.prev': '上一 {lines} 行', 'models.logs.pagination.prev': '上一 {lines} 行',
@@ -247,5 +247,10 @@ export default {
'models.form.generic_proxy.button': '通用代理', 'models.form.generic_proxy.button': '通用代理',
'models.accessControlModal.includeusers': '显示用户', 'models.accessControlModal.includeusers': '显示用户',
'models.table.genericProxy': '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.keys.nofound': '未找到匹配的 API 密钥',
'noresult.catalog.title': '暂无模型', 'noresult.catalog.title': '暂无模型',
'noresult.catalog.subTitle': '尚未配置任何模型。', '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.rerank.query.validate': '查询内容不能为空',
'playground.image.generate.error': '出了一点问题,图片未能生成。', '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 { DoubleRightOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button } from 'antd'; import { Button, FloatButton } from 'antd';
import React from 'react'; import React from 'react';
import { import {
@@ -45,6 +45,7 @@ const InfiniteScroller: React.FC<
</div> </div>
)} )}
</div> </div>
<FloatButton.BackTop visibilityHeight={1000} />
</div> </div>
); );
}; };
@@ -1,7 +1,7 @@
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
interface ScrollerContextProps { interface ScrollerContextProps {
total: number; total: number; // total pages
current: number; current: number;
loading: boolean; loading: boolean;
refresh: (nextPage: number) => void; 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 _ from 'lodash';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -54,9 +54,19 @@ const NoResult: React.FC<
loading?: boolean; loading?: boolean;
loadend?: boolean; loadend?: boolean;
dataSource?: any[]; dataSource?: any[];
buttonText?: React.ReactNode;
onClick?: () => void;
} }
> = (props) => { > = (props) => {
const { filters, noFoundText, loadend, loading, dataSource } = props; const {
filters,
noFoundText,
loadend,
loading,
dataSource,
buttonText,
onClick
} = props;
const hasFilters = useMemo(() => { const hasFilters = useMemo(() => {
const filterValues = _.omit(filters, ['page', 'perPage']); const filterValues = _.omit(filters, ['page', 'perPage']);
@@ -70,6 +80,15 @@ const NoResult: React.FC<
}); });
}, [filters]); }, [filters]);
const renderChildren = () => {
if (!buttonText || !onClick) return null;
return (
<Button color="primary" variant="filled" onClick={onClick}>
{buttonText}
</Button>
);
};
return ( return (
<> <>
{!loading && loadend && !dataSource?.length ? ( {!loading && loadend && !dataSource?.length ? (
@@ -96,7 +115,7 @@ const NoResult: React.FC<
</Description> </Description>
} }
> >
{!hasFilters && props.children} {!hasFilters && renderChildren()}
</StyledEmpty> </StyledEmpty>
) : ( ) : (
<span></span> <span></span>
+4 -6
View File
@@ -6,7 +6,7 @@ import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { Button, ConfigProvider, Table } from 'antd'; import { ConfigProvider, Table } from 'antd';
import { useState } from 'react'; import { useState } from 'react';
import NoResult from '../_components/no-result'; import NoResult from '../_components/no-result';
import PageBox from '../_components/page-box'; import PageBox from '../_components/page-box';
@@ -111,11 +111,9 @@ const APIKeys: React.FC = () => {
})} })}
title={intl.formatMessage({ id: 'noresult.keys.title' })} title={intl.formatMessage({ id: 'noresult.keys.title' })}
subTitle={intl.formatMessage({ id: 'noresult.keys.subTitle' })} subTitle={intl.formatMessage({ id: 'noresult.keys.subTitle' })}
> onClick={handleAddKey}
<Button type="primary" onClick={handleAddKey}> buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
{intl.formatMessage({ id: 'noresult.button.add' })} ></NoResult>
</Button>
</NoResult>
); );
}; };
@@ -2,7 +2,7 @@ import CardSkeleton from '@/components/templates/card-skelton';
import breakpoints from '@/config/breakpoints'; import breakpoints from '@/config/breakpoints';
import InfiniteScroller from '@/pages/_components/infinite-scroller'; import InfiniteScroller from '@/pages/_components/infinite-scroller';
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context'; 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 _ from 'lodash';
import ResizeObserver from 'rc-resize-observer'; import ResizeObserver from 'rc-resize-observer';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
@@ -139,7 +139,6 @@ const CardList: React.FC<BackendListProps> = (props) => {
<ListSkeleton span={span} loading={loading} isFirst={isFirst} /> <ListSkeleton span={span} loading={loading} isFirst={isFirst} />
</InfiniteScroller> </InfiniteScroller>
</ResizeObserver> </ResizeObserver>
<FloatButton.BackTop visibilityHeight={1000} />
</div> </div>
); );
}; };
@@ -1,5 +1,7 @@
import ScrollerModal from '@/components/scroller-modal'; import ScrollerModal from '@/components/scroller-modal';
import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { VersionListItem } from '../config/types'; import { VersionListItem } from '../config/types';
import VersionInfo from '../forms/version-info'; import VersionInfo from '../forms/version-info';
@@ -7,12 +9,14 @@ import VersionInfo from '../forms/version-info';
interface VersionInfoModalProps { interface VersionInfoModalProps {
open?: boolean; open?: boolean;
currentData?: any; currentData?: any;
addVersion?: () => void;
onClose?: () => void; onClose?: () => void;
} }
const VersionInfoModal: React.FC<VersionInfoModalProps> = ({ const VersionInfoModal: React.FC<VersionInfoModalProps> = ({
open, open,
currentData, currentData,
addVersion,
onClose onClose
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@@ -55,7 +59,14 @@ const VersionInfoModal: React.FC<VersionInfoModalProps> = ({
return ( return (
<ScrollerModal <ScrollerModal
open={open} 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} width={600}
centered centered
destroyOnHidden destroyOnHidden
@@ -63,22 +74,6 @@ const VersionInfoModal: React.FC<VersionInfoModalProps> = ({
maskClosable={false} maskClosable={false}
onOk={onClose} onOk={onClose}
onCancel={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} footer={null}
> >
<VersionInfo versionConfigs={versionConfigs} /> <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 vLLMLogo from '@/assets/logo/vllm.png';
import VoxBoxLogo from '@/assets/logo/voxbox.png'; import VoxBoxLogo from '@/assets/logo/voxbox.png';
import icons from '@/components/icon-font/icons'; import icons from '@/components/icon-font/icons';
import { GPUSTACK_API_BASE_URL } from '@/config/settings';
import { backendOptionsMap } from '@/pages/llmodels/config/backend-parameters'; import { backendOptionsMap } from '@/pages/llmodels/config/backend-parameters';
import { import {
GPUDriverMap, GPUDriverMap,
@@ -153,37 +152,37 @@ export const frameworks = [
label: 'CANN', label: 'CANN',
value: GPUDriverMap.ASCEND, value: GPUDriverMap.ASCEND,
tips: ManufacturerMap[GPUDriverMap.ASCEND], tips: ManufacturerMap[GPUDriverMap.ASCEND],
locale: true tipLocale: true
}, },
{ {
label: 'DTK', label: 'DTK',
value: GPUDriverMap.HYGON, value: GPUDriverMap.HYGON,
tips: ManufacturerMap[GPUDriverMap.HYGON], tips: ManufacturerMap[GPUDriverMap.HYGON],
locale: true tipLocale: true
}, },
{ {
label: 'MACA', label: 'MACA',
value: GPUDriverMap.METAX, value: GPUDriverMap.METAX,
tips: ManufacturerMap[GPUDriverMap.METAX], tips: ManufacturerMap[GPUDriverMap.METAX],
locale: true tipLocale: true
}, },
{ {
label: 'CoreX', label: 'CoreX',
value: GPUDriverMap.ILUVATAR, value: GPUDriverMap.ILUVATAR,
tips: ManufacturerMap[GPUDriverMap.ILUVATAR], tips: ManufacturerMap[GPUDriverMap.ILUVATAR],
locale: true tipLocale: true
}, },
{ {
label: 'MUSA', label: 'MUSA',
value: GPUDriverMap.MOORE_THREADS, value: GPUDriverMap.MOORE_THREADS,
tips: ManufacturerMap[GPUDriverMap.MOORE_THREADS], tips: ManufacturerMap[GPUDriverMap.MOORE_THREADS],
locale: true tipLocale: true
}, },
{ {
label: 'Neuware', label: 'Neuware',
value: GPUDriverMap.CAMBRICON, value: GPUDriverMap.CAMBRICON,
tips: ManufacturerMap[GPUDriverMap.CAMBRICON], tips: ManufacturerMap[GPUDriverMap.CAMBRICON],
locale: true tipLocale: true
}, },
{ {
label: 'CPU', 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 backend_name: vllm-custom
description: this is my custom vllm backend description: this is my custom vllm backend
default_version: v0.11.0 default_version: v0.11.0
health_check_path: /${GPUSTACK_API_BASE_URL}/models health_check_path: /v1/models
default_backend_param: default_backend_param:
- --host - --host
default_run_command: vllm serve {{model_path}} --port {{port}} --host {{worker_ip}} --served-model-name {{model_name}} 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; top: 0;
width: 100%; width: 100%;
z-index: 100; z-index: 100;
padding-bottom: 16px; margin-block: 16px;
padding-top: 16px;
background-color: var(--ant-color-bg-container); background-color: var(--ant-color-bg-container);
`; `;
+1 -1
View File
@@ -163,7 +163,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
<span> <span>
{option.label} {option.label}
{data.tips ? ( {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">{` [${intl.formatMessage({ id: data.tips })}]`}</span>
) : ( ) : (
<span className="text-tertiary m-l-4">{` [${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 useTableFetch from '@/hooks/use-table-fetch';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { Button } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { useState } from 'react'; import { useState } from 'react';
import { ScrollerContext } from '../_components/infinite-scroller/use-scroller-context'; 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) => { const loadMore = useMemoizedFn((nextPage: number) => {
fetchData({ fetchData({
query: { query: {
@@ -209,11 +220,9 @@ const BackendList = () => {
})} })}
title={intl.formatMessage({ id: 'noresult.backend.title' })} title={intl.formatMessage({ id: 'noresult.backend.title' })}
subTitle={intl.formatMessage({ id: 'noresult.backend.subTitle' })} subTitle={intl.formatMessage({ id: 'noresult.backend.subTitle' })}
> onClick={handleAddBackend}
<Button type="primary" onClick={handleAddBackend}> buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
{intl.formatMessage({ id: 'noresult.button.add' })} ></NoResult>
</Button>
</NoResult>
</ScrollerContext.Provider> </ScrollerContext.Provider>
<AddModal <AddModal
action={openModalStatus.action} action={openModalStatus.action}
@@ -229,6 +238,7 @@ const BackendList = () => {
} }
></AddModal> ></AddModal>
<VersionInfoModal <VersionInfoModal
addVersion={handleAddVersion}
open={openVersionInfoModal.open} open={openVersionInfoModal.open}
currentData={openVersionInfoModal.currentData as ListItem} currentData={openVersionInfoModal.currentData as ListItem}
onClose={() => onClose={() =>
@@ -1,7 +1,9 @@
import { clusterSessionAtom } from '@/atoms/clusters';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import PageBreadcrumb from '@/pages/_components/page-breadcrumb'; import PageBreadcrumb from '@/pages/_components/page-breadcrumb';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -16,6 +18,7 @@ import FooterButtons from './components/footer-buttons';
import ProviderCatalog from './components/provider-catalog'; import ProviderCatalog from './components/provider-catalog';
import { ProviderType, ProviderValueMap } from './config'; import { ProviderType, ProviderValueMap } from './config';
import providerList from './config/providers'; import providerList from './config/providers';
import { StepsContext } from './config/steps-context';
import { ClusterFormData } from './config/types'; import { ClusterFormData } from './config/types';
import { moduleMap, moduleRegistry } from './step-forms/module-registry'; import { moduleMap, moduleRegistry } from './step-forms/module-registry';
import useStepList from './step-forms/use-step-list'; import useStepList from './step-forms/use-step-list';
@@ -39,6 +42,7 @@ const ClusterCreate = () => {
const action = const action =
(searchParams.get('action') as PageActionType) || PageAction.CREATE; (searchParams.get('action') as PageActionType) || PageAction.CREATE;
const navigate = useNavigate(); const navigate = useNavigate();
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
const [credentialList, setCredentialList] = useState< const [credentialList, setCredentialList] = useState<
Global.BaseOption<number, { provider: ProviderType }>[] Global.BaseOption<number, { provider: ProviderType }>[]
>([]); >([]);
@@ -86,6 +90,7 @@ const ClusterCreate = () => {
})); }));
}, [action, extraData.provider, stepList]); }, [action, extraData.provider, stepList]);
// before moving to the next step, get all form values
const getFormFieldsValue = () => { const getFormFieldsValue = () => {
setFormValues((prev) => { setFormValues((prev) => {
const newFormValues = _.cloneDeep(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 * @returns
*/ */
const renderModules = () => { const renderModules = () => {
@@ -253,6 +258,14 @@ const ClusterCreate = () => {
}; };
const handleCancel = () => { const handleCancel = () => {
if (clusterSession?.firstAddCluster) {
setClusterSession({
firstAddCluster: false,
firstAddWorker: false
});
navigate(`/cluster-management/clusters/list`);
return;
}
navigate(-1); navigate(-1);
}; };
@@ -293,10 +306,16 @@ const ClusterCreate = () => {
current={extraData.provider} current={extraData.provider}
/> />
)} )}
<StepsContext.Provider
value={{
formValues: formValues
}}
>
{renderModules()} {renderModules()}
<Container> <Container>
<Content>{renderForms()}</Content> <Content>{renderForms()}</Content>
</Container> </Container>
</StepsContext.Provider>
</div> </div>
</PageContainerInner> </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 DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools'; 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 useWatchList from '@/hooks/use-watch-list';
import { useIntl, useNavigate } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, message } from 'antd'; import { message } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import NoResult from '../_components/no-result'; import NoResult from '../_components/no-result';
@@ -33,7 +33,11 @@ import {
K8sStepsFromCluter K8sStepsFromCluter
} from './components/add-worker/config'; } from './components/add-worker/config';
import PoolRows from './components/pool-rows'; import PoolRows from './components/pool-rows';
import { ProviderType, ProviderValueMap } from './config'; import {
ClusterStatusValueMap,
ProviderType,
ProviderValueMap
} from './config';
import { import {
ClusterListItem, ClusterListItem,
CredentialListItem, CredentialListItem,
@@ -65,6 +69,7 @@ const Clusters: React.FC = () => {
}); });
const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API); const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API);
const [expandAtom] = useAtom(expandKeysAtom); const [expandAtom] = useAtom(expandKeysAtom);
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
const { handleExpandChange, handleExpandAll, expandedRowKeys } = const { handleExpandChange, handleExpandAll, expandedRowKeys } =
useExpandedRowKeys(expandAtom); useExpandedRowKeys(expandAtom);
const navigate = useNavigate(); 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}`); navigate(`/cluster-management/clusters/create?action=${PageAction.CREATE}`);
}; };
@@ -241,6 +246,35 @@ const Clusters: React.FC = () => {
fetchCredentialList(); 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 = ( const renderChildren = (
list: any, list: any,
options: { parent?: any; [key: string]: any } options: { parent?: any; [key: string]: any }
@@ -305,11 +339,9 @@ const Clusters: React.FC = () => {
subTitle={intl.formatMessage({ subTitle={intl.formatMessage({
id: 'noresult.cluster.subTitle' id: 'noresult.cluster.subTitle'
})} })}
> onClick={handleClickDropdown}
<Button type="primary" onClick={handleClickDropdown}> buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
{intl.formatMessage({ id: 'noresult.button.add' })} ></NoResult>
</Button>
</NoResult>
} }
pagination={{ pagination={{
showSizeChanger: true, showSizeChanger: true,
@@ -7,12 +7,24 @@ import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import styled from 'styled-components';
import { ProviderType, ProviderValueMap } from '../config'; import { ProviderType, ProviderValueMap } from '../config';
import { import {
CredentialFormData as FormData, CredentialFormData as FormData,
CredentialListItem as ListItem CredentialListItem as ListItem
} from '../config/types'; } 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 = { type AddModalProps = {
title: string; title: string;
action: PageActionType; action: PageActionType;
@@ -100,8 +112,24 @@ const AddModal: React.FC<AddModalProps> = ({
]} ]}
> >
<SealInput.Password <SealInput.Password
label={intl.formatMessage({ id: 'clusters.credential.token' })} label={intl.formatMessage({
id: 'clusters.credential.token'
})}
required={action === PageAction.CREATE} 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> ></SealInput.Password>
</Form.Item> </Form.Item>
</> </>
@@ -45,7 +45,7 @@ const SelectVendor = () => {
updateField('currentGPU', GPUDriverMap.NVIDIA); updateField('currentGPU', GPUDriverMap.NVIDIA);
updateField('workerCommand', { updateField('workerCommand', {
label: 'NVIDIA', 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] notes: AddWorkerDockerNotes[GPUDriverMap.NVIDIA]
}); });
}, []); }, []);
@@ -17,6 +17,7 @@ import useAppUtils from '@/hooks/use-app-utils';
import { CardContainer } from '@/pages/llmodels/components/gpu-card'; import { CardContainer } from '@/pages/llmodels/components/gpu-card';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button, Form } from 'antd'; import { Button, Form } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
@@ -24,6 +25,7 @@ import React, {
forwardRef, forwardRef,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useMemo,
useState useState
} from 'react'; } from 'react';
import styled from 'styled-components'; 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: { const RenderLabel = (data: {
label: React.ReactNode; label: React.ReactNode;
vendor: string; vendor: string;
@@ -191,37 +184,45 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
useEffect(() => { useEffect(() => {
if (currentData) { 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({ 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: { setInstanceSpec(() => {
label: React.ReactNode; return selectInstanceType ? currentData.instance_spec : {};
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>
);
} }
const selectImage = osImageList.find((item) => item.value === data.value); }, [currentData, instanceTypeList, osImageList]);
if (selectImage) {
return ( const updateImageList = useMemoizedFn((instanceSpec: Record<string, any>) => {
<RenderLabel label={data.label} vendor={selectImage.vendor || ''} /> 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: { const instanceLabelRender = (data: {
label: React.ReactNode; label: React.ReactNode;
@@ -230,6 +231,10 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
const currentInstanceSpec = const currentInstanceSpec =
instanceTypeList.find((item) => item.value === data.value) || instanceTypeList.find((item) => item.value === data.value) ||
instanceSpec; instanceSpec;
if (!currentInstanceSpec || _.isEmpty(currentInstanceSpec)) {
return null;
}
return ( return (
<RenderLabel <RenderLabel
label={data.label || currentInstanceSpec?.label || data.value} 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({ form.setFieldsValue({
image_name: os_image: option.os_image || value
osImageList.find((item) => item.value === value)?.label || value
}); });
}; };
const handleInstanceTypeChange = (value: string, option: any) => { const handleInstanceTypeChange = (value: string, option: any) => {
setInstanceSpec({ const newInstanceSpec = {
...option.specInfo, ...option.specInfo,
label: option.label, label: option.label,
vendor: option.vendor, vendor: option.vendor,
description: option.description description: option.description,
}); count: option.count
};
setInstanceSpec({ ...newInstanceSpec });
const newImageList = updateImageList({ ...newInstanceSpec });
form.setFieldsValue({ form.setFieldsValue({
instance_spec: { os_image: newImageList[0]?.os_image,
...option.specInfo, image_name: newImageList[0]?.value,
label: option.label, instance_spec: { ...newInstanceSpec }
vendor: option.vendor,
description: option.description
}
}); });
}; };
@@ -402,7 +408,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
</Form.Item> </Form.Item>
<Form.Item<FormData> <Form.Item<FormData>
name="os_image" name="image_name"
rules={[ rules={[
{ {
required: true, required: true,
@@ -420,9 +426,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
styles: { header: { marginBlock: 5 } } styles: { header: { marginBlock: 5 } }
}) })
} }
labelRender={imageLabelRender} options={imageList}
placeholder={currentData?.image_name}
options={osImageList}
disabled={action === PageAction.EDIT} disabled={action === PageAction.EDIT}
label={intl.formatMessage({ label={intl.formatMessage({
id: 'clusters.workerpool.osImage' id: 'clusters.workerpool.osImage'
@@ -462,7 +466,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
></LabelSelector> ></LabelSelector>
</Form.Item> </Form.Item>
<VolumesConfig disabled={action === PageAction.EDIT}></VolumesConfig> <VolumesConfig disabled={action === PageAction.EDIT}></VolumesConfig>
<Form.Item<FormData> name="image_name" hidden> <Form.Item<FormData> name="os_image" hidden>
<SealInput.Input></SealInput.Input> <SealInput.Input></SealInput.Input>
</Form.Item> </Form.Item>
<InstanceSpecData instanceSpec={instanceSpec} /> <InstanceSpecData instanceSpec={instanceSpec} />
@@ -57,7 +57,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
key: GPUDriverMap.NVIDIA, key: GPUDriverMap.NVIDIA,
locale: false, locale: false,
notes: AddWorkerDockerNotes[GPUDriverMap.NVIDIA], 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 }} /> icon: <IconFont type="icon-nvidia2" style={{ fontSize: 32 }} />
}, },
{ {
@@ -67,7 +67,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
key: GPUDriverMap.AMD, key: GPUDriverMap.AMD,
locale: false, locale: false,
notes: AddWorkerDockerNotes[GPUDriverMap.AMD], 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: ( icon: (
<IconFont <IconFont
type="icon-amd" type="icon-amd"
@@ -82,17 +82,17 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
key: GPUDriverMap.ASCEND, key: GPUDriverMap.ASCEND,
locale: false, locale: false,
notes: AddWorkerDockerNotes[GPUDriverMap.ASCEND], 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 /> icon: <ProviderImage src={ascendLogo} showBg />
}, },
{ {
label: intl.formatMessage({ id: 'vendor.hygon' }), label: intl.formatMessage({ id: 'vendor.hygon' }),
description: 'common.tag.experimental', description: '',
value: GPUDriverMap.HYGON, value: GPUDriverMap.HYGON,
key: GPUDriverMap.HYGON, key: GPUDriverMap.HYGON,
locale: false, locale: false,
notes: AddWorkerDockerNotes[GPUDriverMap.HYGON], 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} /> icon: <ProviderImage src={hyponPNG} />
}, },
{ {
@@ -102,7 +102,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
key: GPUDriverMap.MOORE_THREADS, key: GPUDriverMap.MOORE_THREADS,
locale: false, locale: false,
notes: AddWorkerDockerNotes[GPUDriverMap.MOORE_THREADS], 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} /> icon: <ProviderImage src={moorePNG} />
}, },
{ {
@@ -112,7 +112,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
key: GPUDriverMap.ILUVATAR, key: GPUDriverMap.ILUVATAR,
locale: false, locale: false,
notes: AddWorkerDockerNotes[GPUDriverMap.ILUVATAR], 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 /> icon: <ProviderImage src={iluvatarWEBP} showBg />
}, },
{ {
@@ -122,7 +122,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
key: GPUDriverMap.CAMBRICON, key: GPUDriverMap.CAMBRICON,
locale: false, locale: false,
notes: AddWorkerDockerNotes[GPUDriverMap.CAMBRICON], 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} /> icon: <ProviderImage src={CambriconPNG} />
}, },
{ {
@@ -131,6 +131,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
value: GPUDriverMap.METAX, value: GPUDriverMap.METAX,
key: GPUDriverMap.METAX, key: GPUDriverMap.METAX,
locale: false, locale: false,
link: 'https://docs.gpustack.ai/latest/installation/metax/installation/?h=meta#prerequisites',
notes: AddWorkerDockerNotes[GPUDriverMap.METAX], notes: AddWorkerDockerNotes[GPUDriverMap.METAX],
icon: <ProviderImage src={metaxLogo} showBg /> 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);
+3 -8
View File
@@ -7,7 +7,7 @@ import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import { useIntl, useNavigate } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, ConfigProvider, Table, message } from 'antd'; import { ConfigProvider, Table, message } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import NoResult from '../_components/no-result'; import NoResult from '../_components/no-result';
@@ -164,14 +164,9 @@ const Credentials: React.FC = () => {
subTitle={intl.formatMessage({ subTitle={intl.formatMessage({
id: 'noresult.credentials.subTitle' id: 'noresult.credentials.subTitle'
})} })}
>
<Button
type="primary"
onClick={() => handleAddCredential(addActions[0])} onClick={() => handleAddCredential(addActions[0])}
> buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
{intl.formatMessage({ id: 'noresult.button.add' })} ></NoResult>
</Button>
</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 label = formatLabel(item);
const description = `${label} ${item.gpu_info?.count}X`; const description = `${label} ${item.gpu_info?.count}X`;
return { return {
count: item.gpu_info?.count,
label: `${description} - ${formatSpec(specInfo)}`, label: `${description} - ${formatSpec(specInfo)}`,
value: item.slug, value: item.slug,
description: description, description: description,
@@ -150,7 +151,8 @@ export const useProviderRegions = () => {
.map((item: any) => { .map((item: any) => {
return { return {
label: item.description, label: item.description,
value: item.slug, value: item.description,
os_image: item.slug,
name: item.name, name: item.name,
description: item.description, description: item.description,
vendor: _.camelCase(item.distribution), vendor: _.camelCase(item.distribution),
@@ -171,9 +173,12 @@ export const useProviderRegions = () => {
}; };
const updateOSImages = (region: string, allImages?: any[]) => { const updateOSImages = (region: string, allImages?: any[]) => {
const list = (allImages || allOSImageList).filter((item) => const list = (allImages || allOSImageList).filter(
item.regions.includes(region) (item) =>
item.regions.includes(region) &&
['debian', 'ubuntu'].includes(item.vendor)
); );
console.log('osimagelist========', list);
setOSImageList(list); setOSImageList(list);
}; };
@@ -108,7 +108,6 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
} }
return {}; return {};
}); });
console.log('gatherFormValues========', resultList);
return resultList.filter((item) => item); return resultList.filter((item) => item);
}; };
@@ -135,7 +134,6 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
const newWorkerPools = worker_pools.map( const newWorkerPools = worker_pools.map(
(poolData: NodePoolFormData, index: number) => [index, poolData] (poolData: NodePoolFormData, index: number) => [index, poolData]
); );
console.log('newWorkerPools========', newWorkerPools);
setWorkerPoolList(new Map(newWorkerPools)); setWorkerPoolList(new Map(newWorkerPools));
}; };
@@ -143,14 +141,12 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
const values = Object.values(formRefs.current).map((form) => const values = Object.values(formRefs.current).map((form) =>
form?.getFieldsValue() form?.getFieldsValue()
); );
console.log('getFieldsValue========', values);
return { return {
worker_pools: values worker_pools: values
}; };
}; };
const handleOnToggle = (open: boolean, key: number) => { const handleOnToggle = (open: boolean, key: number) => {
console.log('Active keys changed:', key);
if (open) { if (open) {
setActiveKey((prev) => new Set([key])); setActiveKey((prev) => new Set([key]));
} else { } else {
@@ -170,7 +166,6 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
useEffect(() => { useEffect(() => {
if (currentData) { if (currentData) {
console.log('currentData===========1=', currentData);
setFieldsValue(currentData); setFieldsValue(currentData);
} }
}, [currentData]); }, [currentData]);
@@ -2,11 +2,12 @@
box-shadow: none !important; box-shadow: none !important;
:global(.ant-card-body) { :global(.ant-card-body) {
height: 110px; height: 96px;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
border-radius: var(--ant-border-radius-lg); border-radius: var(--ant-border-radius-lg);
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
padding: 16px 24px;
} }
} }
@@ -1,4 +1,5 @@
import AlertBlockInfo from '@/components/alert-info/block'; import AlertBlockInfo from '@/components/alert-info/block';
import TooltipList from '@/components/tooltip-list';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import TransferInner from '@/pages/_components/transfer'; import TransferInner from '@/pages/_components/transfer';
@@ -29,7 +30,34 @@ import { AccessControlFormData, ListItem } from '../../config/types';
type TransferKey = string | number | bigint; 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` const Label = styled.div`
display: flex;
align-items: center;
gap: 4px;
font-weight: 500; font-weight: 500;
margin-block: 8px 12px; margin-block: 8px 12px;
font-size: 14px; font-size: 14px;
@@ -263,7 +291,13 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
access_policy: action === PageAction.CREATE ? 'authed' : undefined 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> <Form.Item<AccessControlFormData> name="access_policy" noStyle>
<Radio.Group <Radio.Group
onChange={handleOnPolicyChange} onChange={handleOnPolicyChange}
@@ -307,7 +341,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
id: 'models.table.userSelection.tips' id: 'models.table.userSelection.tips'
})} })}
> >
<QuestionCircleOutlined style={{ marginLeft: 4 }} /> <QuestionCircleOutlined />
</Tooltip> </Tooltip>
</Label> </Label>
<Form.Item<AccessControlFormData> name="users"> <Form.Item<AccessControlFormData> name="users">
@@ -3,7 +3,7 @@ import breakpoints from '@/config/breakpoints';
import InfiniteScroller from '@/pages/_components/infinite-scroller'; import InfiniteScroller from '@/pages/_components/infinite-scroller';
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context'; import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Col, FloatButton, Row, Spin } from 'antd'; import { Col, Row, Spin } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import ResizeObserver from 'rc-resize-observer'; import ResizeObserver from 'rc-resize-observer';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
@@ -106,7 +106,6 @@ const CatalogList: React.FC<CatalogListProps> = (props) => {
<ListSkeleton span={span} loading={loading} isFirst={isFirst} /> <ListSkeleton span={span} loading={loading} isFirst={isFirst} />
</InfiniteScroller> </InfiniteScroller>
</ResizeObserver> </ResizeObserver>
<FloatButton.BackTop visibilityHeight={1000} />
</div> </div>
); );
}; };
@@ -177,9 +177,9 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}; };
const initClusterId = (): number => { const initClusterId = (): number => {
const cluster_id = clusterList?.find( const cluster_id =
(item) => item.state === ClusterStatusValueMap.Ready clusterList?.find((item) => item.state === ClusterStatusValueMap.Ready)
)?.value; ?.value || clusterList?.[0]?.value;
return cluster_id as number; return cluster_id as number;
}; };
+13 -5
View File
@@ -110,7 +110,6 @@ const AddModal: FC<AddModalProps> = (props) => {
const { checkOnlyAscendNPU } = useCheckBackend(); const { checkOnlyAscendNPU } = useCheckBackend();
const { const {
handleShowCompatibleAlert,
setWarningStatus, setWarningStatus,
handleBackendChangeBefore, handleBackendChangeBefore,
cancelEvaluate, cancelEvaluate,
@@ -258,7 +257,10 @@ const AddModal: FC<AddModalProps> = (props) => {
return; return;
} }
const modelInfo = onSelectModel(selectedModel, props.source); const modelInfo = onSelectModel(selectedModel, {
source: props.source,
defaultBackend: form.current?.getFieldValue?.('backend')
});
form.current?.setFieldsValue?.({ form.current?.setFieldsValue?.({
..._.omit(modelInfo, ['name']), ..._.omit(modelInfo, ['name']),
@@ -323,13 +325,17 @@ const AddModal: FC<AddModalProps> = (props) => {
// TODO // TODO
form.current?.resetFields(resetFields); form.current?.resetFields(resetFields);
const modelInfo = onSelectModel(item, props.source); const modelInfo = onSelectModel(item, {
source: props.source
});
form.current?.setFieldsValue?.({ form.current?.setFieldsValue?.({
...defaultFormValues, ...defaultFormValues,
...modelInfo, ...modelInfo,
categories: getCategory(item) categories: getCategory(item)
}); });
console.log('modelInfo:', modelInfo);
let warningStatus: MessageStatus = { let warningStatus: MessageStatus = {
show: true, show: true,
title: '', title: '',
@@ -359,7 +365,9 @@ const AddModal: FC<AddModalProps> = (props) => {
requestModelId: updateRequestModelId() requestModelId: updateRequestModelId()
}); });
handleCancelFiles(); handleCancelFiles();
const modelInfo = onSelectModel(item, props.source); const modelInfo = onSelectModel(item, {
source: props.source
});
if ( if (
evaluateStateRef.current.state === EvaluateProccess.model && evaluateStateRef.current.state === EvaluateProccess.model &&
@@ -435,7 +443,7 @@ const AddModal: FC<AddModalProps> = (props) => {
} }
const cluster_id = const cluster_id =
clusterList?.find((item) => item.state === ClusterStatusValueMap.Ready) clusterList?.find((item) => item.state === ClusterStatusValueMap.Ready)
?.value || ''; ?.value || clusterList?.[0]?.value;
return cluster_id; return cluster_id;
}; };
@@ -94,6 +94,9 @@ const draftModelDownloadList: ColumnProps[] = [
title: 'models.form.draftModel', title: 'models.form.draftModel',
locale: true, locale: true,
key: 'draft_model', key: 'draft_model',
style: {
wordBreak: 'break-word'
},
width: 280 width: 280
}, },
...statusColumn ...statusColumn
@@ -580,7 +580,14 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
return ( return (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<div className={SearchStyle['search-bar']}>{renderHFSearch()}</div> <div className={SearchStyle['search-bar']}>{renderHFSearch()}</div>
<ColumnWrapper maxHeight={'calc(100vh - 210px)'}> <ColumnWrapper
maxHeight={'calc(100vh - 210px)'}
styles={{
container: {
paddingTop: 0
}
}}
>
<SearchResult <SearchResult
loading={dataSource.loading} loading={dataSource.loading}
resultList={dataSource.dataList} resultList={dataSource.dataList}
+5 -8
View File
@@ -429,6 +429,7 @@ const Models: React.FC<ModelsProps> = ({
data: row data: row
}); });
}, []); }, []);
const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => { const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => {
try { try {
if (val === 'edit') { if (val === 'edit') {
@@ -716,15 +717,11 @@ const Models: React.FC<ModelsProps> = ({
subTitle={intl.formatMessage({ subTitle={intl.formatMessage({
id: 'noresult.deployments.subTitle' id: 'noresult.deployments.subTitle'
})} })}
>
<Button
type="primary"
iconPosition="end"
onClick={() => handleClickDropdown({ key: 'catalog' })} onClick={() => handleClickDropdown({ key: 'catalog' })}
> buttonText={intl?.formatMessage?.({
{intl?.formatMessage?.({ id: 'models.table.button.deploy' })} id: 'models.table.button.deploy'
</Button> })}
</NoResult> ></NoResult>
} }
pagination={{ pagination={{
showSizeChanger: true, showSizeChanger: true,
+6 -1
View File
@@ -307,7 +307,12 @@ export interface BackendOption {
default_backend_param: string[]; default_backend_param: string[];
default_version: string; default_version: string;
isBuiltIn: boolean; isBuiltIn: boolean;
versions: { label: string; value: string; title?: string }[]; versions: {
label: string;
value: string;
title?: string;
is_deprecated: boolean;
}[];
} }
export interface AccessControlFormData { export interface AccessControlFormData {
@@ -39,6 +39,7 @@ const BackendParametersList: React.FC = () => {
return ( return (
<Form.Item<FormData> name="backend_parameters"> <Form.Item<FormData> name="backend_parameters">
<ListInput <ListInput
trim={false}
placeholder={ placeholder={
backendParamsHolderTips[backend] backendParamsHolderTips[backend]
? intl.formatMessage({ ? intl.formatMessage({
+124 -27
View File
@@ -1,19 +1,35 @@
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import TooltipList from '@/components/tooltip-list'; import TooltipList from '@/components/tooltip-list';
import useAppUtils from '@/hooks/use-app-utils'; import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max'; import { CaretDownOutlined, InfoCircleOutlined } from '@ant-design/icons';
import { Form } from 'antd'; import { useIntl, useNavigate } from '@umijs/max';
import { Form, Select } from 'antd';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import styled from 'styled-components';
import { backendTipsList } from '../config'; import { backendTipsList } from '../config';
import { backendOptionsMap } from '../config/backend-parameters'; import { backendOptionsMap } from '../config/backend-parameters';
import { useFormContext } from '../config/form-context'; 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 BackendFields: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigate();
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const { onValuesChange, backendOptions, onBackendChange } = useFormContext(); const { onValuesChange, backendOptions, onBackendChange } = useFormContext();
const backend = Form.useWatch('backend', form); const backend = Form.useWatch('backend', form);
const [showDeprecated, setShowDeprecated] = React.useState<boolean>(false);
const handleBackendVersionOnChange = (value: any) => { const handleBackendVersionOnChange = (value: any) => {
onValuesChange?.({}, form.getFieldsValue()); onValuesChange?.({}, form.getFieldsValue());
@@ -45,9 +61,17 @@ const BackendFields: React.FC = () => {
return options; return options;
}, [backendOptions, intl]); }, [backendOptions, intl]);
const backendVersions = useMemo(() => { const backendVersions = useMemo((): {
builtIn: any[];
custom: any[];
deprecated: any[];
} => {
if (!backend || backend === backendOptionsMap.custom) { if (!backend || backend === backendOptionsMap.custom) {
return []; return {
builtIn: [],
custom: [],
deprecated: []
};
} }
// find the backend item from backendOptions // find the backend item from backendOptions
@@ -57,34 +81,33 @@ const BackendFields: React.FC = () => {
// if it's a custom backend, // if it's a custom backend,
if (backendItem && !backendItem.isBuiltIn) { 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 // 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( const builtInVersions = versions.filter(
(item) => !item.value?.endsWith('-custom') (item) => !item.value?.endsWith('-custom') && !item.is_deprecated
);
const customVersions = versions.filter((item) =>
item.value?.endsWith('-custom')
); );
const options = []; // ============ Custom Versions ============
const customVersions = versions.filter(
(item) => item.value?.endsWith('-custom') && !item.is_deprecated
);
if (builtInVersions.length > 0) { // ============ Deprecated Versions ============
options.push({ const deprecatedVersions = versions.filter((item) => item.is_deprecated);
label: intl.formatMessage({ id: 'backend.builtin' }),
options: builtInVersions
});
}
if (customVersions.length > 0) { return {
options.push({ builtIn: builtInVersions,
label: intl.formatMessage({ id: 'models.form.backend.custom' }), custom: customVersions,
options: customVersions deprecated: deprecatedVersions
}); };
}
return options;
}, [backend, backendOptions, intl]); }, [backend, backendOptions, intl]);
const optionRender = (option: any) => { const optionRender = (option: any) => {
@@ -95,6 +118,50 @@ const BackendFields: React.FC = () => {
return option.title; 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 ( return (
<> <>
<Form.Item <Form.Item
@@ -120,15 +187,45 @@ const BackendFields: React.FC = () => {
<Form.Item name="backend_version"> <Form.Item name="backend_version">
<SealSelect <SealSelect
allowClear allowClear
options={backendVersions} showSearch
optionRender={optionRender}
labelRender={labelRender} labelRender={labelRender}
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
id: 'models.form.backendVersion.holder' id: 'models.form.backendVersion.holder'
})} })}
onChange={handleBackendVersionOnChange} onChange={handleBackendVersionOnChange}
label={intl.formatMessage({ id: 'models.form.backendVersion' })} 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> </Form.Item>
)} )}
</> </>
-1
View File
@@ -51,7 +51,6 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
item.state === ClusterStatusValueMap.Ready item.state === ClusterStatusValueMap.Ready
? item.label ? item.label
: `${item.label} [${ClusterStatusLabelMap[item.state as string]}]`, : `${item.label} [${ClusterStatusLabelMap[item.state as string]}]`,
disabled: item.state !== ClusterStatusValueMap.Ready,
value: item.value value: item.value
}; };
}); });
@@ -47,6 +47,7 @@ const CustomBackend: React.FC = () => {
<SealInput.Input <SealInput.Input
required required
allowClear allowClear
onBlur={handleImageNameOnBlur}
label={intl.formatMessage({ id: 'backend.imageName' })} label={intl.formatMessage({ id: 'backend.imageName' })}
></SealInput.Input> ></SealInput.Input>
</Form.Item> </Form.Item>
@@ -65,6 +66,7 @@ const CustomBackend: React.FC = () => {
scaleSize={false} scaleSize={false}
alwaysFocus={true} alwaysFocus={true}
autoSize={{ minRows: 2, maxRows: 4 }} autoSize={{ minRows: 2, maxRows: 4 }}
onBlur={handleRunCommandOnBlur}
label={intl.formatMessage({ id: 'backend.runCommand' })} label={intl.formatMessage({ id: 'backend.runCommand' })}
description={intl.formatMessage({ description={intl.formatMessage({
id: 'backend.form.defaultExecuteCommand.tips' id: 'backend.form.defaultExecuteCommand.tips'
+17 -2
View File
@@ -173,6 +173,16 @@ export const useCheckCompatibility = () => {
const handleEvaluate = async (data: any) => { const handleEvaluate = async (data: any) => {
try { 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?.cancel();
checkTokenRef.current = createAxiosToken(); checkTokenRef.current = createAxiosToken();
setWarningStatus({ setWarningStatus({
@@ -407,6 +417,7 @@ export const useCheckCompatibility = () => {
return; return;
} }
// when custom backend, and no run_command or image_name, skip evaluate
if ( if (
backendOptionsMap.custom === allValues.backend && backendOptionsMap.custom === allValues.backend &&
(!allValues.run_command || !allValues.image_name) (!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. // just for setting the model name or repo_id, and the backend, Since the model type is fixed.
const { gpuOptions } = data; 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]; let name = _.split(selectModel.name, '/').slice(-1)[0];
const reg = /(-gguf)$/i; const reg = /(-gguf)$/i;
name = _.toLower(name).replace(reg, ''); name = _.toLower(name).replace(reg, '');
@@ -512,7 +527,7 @@ export const useSelectModel = (data: { gpuOptions: any[] }) => {
const modelTaskData = recognizeAudioModel(selectModel, source); const modelTaskData = recognizeAudioModel(selectModel, source);
const backend = checkCurrentbackend({ const backend = checkCurrentbackend({
defaultBackend: backendOptionsMap.vllm, defaultBackend: defaultBackend || backendOptionsMap.vllm,
isAudio: modelTaskData.type === modelTaskMap.audio, isAudio: modelTaskData.type === modelTaskMap.audio,
isGGUF: selectModel.isGGUF, isGGUF: selectModel.isGGUF,
gpuOptions: gpuOptions gpuOptions: gpuOptions
@@ -2,7 +2,7 @@
import AutoTooltip from '@/components/auto-tooltip'; import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons'; import DropdownButtons from '@/components/drop-down-buttons';
import { SealColumnProps } from '@/components/seal-table/types'; 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 { QuestionCircleOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
@@ -94,7 +94,7 @@ const useModelsColumns = ({
<Tooltip <Tooltip
title={intl.formatMessage( title={intl.formatMessage(
{ id: 'models.form.replicas.tips' }, { 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)' }}> <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_backend_param: item.default_backend_param || [],
default_version: item.default_version, default_version: item.default_version,
isBuiltIn: item.is_built_in, isBuiltIn: item.is_built_in,
versions: (item.versions || []).map((vItem) => ({ versions: (item.versions || []).map((vItem, index) => ({
label: vItem.version, label: vItem.version,
value: vItem.version, value: vItem.version,
is_deprecated: vItem.is_deprecated,
title: vItem.version.replace(/-custom$/, '') title: vItem.version.replace(/-custom$/, '')
})) }))
}; };
+1 -1
View File
@@ -88,7 +88,7 @@ const useStyles = createStyles(({ token, css }) => ({
const LoginForm = () => { const LoginForm = () => {
const [messageApi, contextHolder] = message.useMessage(); const [messageApi, contextHolder] = message.useMessage();
const { styles } = useStyles(); const { styles } = useStyles();
const [userInfo, setUserInfo] = useAtom(userAtom); const [, setUserInfo] = useAtom(userAtom);
const { initialState, setInitialState } = useModel('@@initialState') || {}; const { initialState, setInitialState } = useModel('@@initialState') || {};
const [authError, setAuthError] = useState<Error | null>(null); const [authError, setAuthError] = useState<Error | null>(null);
const intl = useIntl(); const intl = useIntl();
+8 -2
View File
@@ -1,7 +1,11 @@
import { initialPasswordAtom, userAtom } from '@/atoms/user'; import { initialPasswordAtom, userAtom } from '@/atoms/user';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import { PasswordReg } from '@/config'; 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 { LockOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Form, message } from 'antd'; import { Button, Form, message } from 'antd';
@@ -34,10 +38,12 @@ const PasswordForm: React.FC = () => {
}); });
await setUserInfo({ await setUserInfo({
...userInfo, ...(userInfo || {}),
require_password_change: false require_password_change: false
}); });
setInitialPassword(''); setInitialPassword('');
// Reset first login flag
writeState(IS_FIRST_LOGIN, null);
gotoDefaultPage(userInfo); gotoDefaultPage(userInfo);
message.success(intl.formatMessage({ id: 'common.message.success' })); message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) { } catch (error) {
@@ -201,9 +201,11 @@ const GroundSTT: React.FC<MessageProps> = forwardRef((props, ref) => {
const handleUploadChange = useCallback( const handleUploadChange = useCallback(
async (data: { file: any; fileList: any }) => { async (data: { file: any; fileList: any }) => {
try {
const res = await readAudioFile(data.file); const res = await readAudioFile(data.file);
setAudioData(res); setAudioData(res);
setTokenResult(null); setTokenResult(null);
} catch (error) {}
}, },
[] []
); );
@@ -28,9 +28,9 @@ import { useHotkeys } from 'react-hotkeys-hook';
import styled from 'styled-components'; import styled from 'styled-components';
import { Roles } from '../config'; import { Roles } from '../config';
import { AudioFormat, MessageItem } from '../config/types'; import { AudioFormat, MessageItem } from '../config/types';
import useAddImage from '../hooks/use-add-image';
import '../style/message-input.less'; import '../style/message-input.less';
import ThumbImg from './thumb-img'; import ThumbImg from './thumb-img';
import UploadImg from './upload-img';
const AudioWrapper = styled.div` const AudioWrapper = styled.div`
padding-block: 10px; padding-block: 10px;
@@ -163,8 +163,11 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
content: '', content: '',
imgs: [] imgs: []
}); });
const [isFromUrl, setIsFromUrl] = useState(false);
const [openImgTips, setOpenImgTips] = useState(false);
const uidCountRef = useRef(0); const uidCountRef = useRef(0);
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
const inputImgRef = useRef<any>(null);
const updateUidCount = () => { const updateUidCount = () => {
uidCountRef.current += 1; uidCountRef.current += 1;
@@ -358,6 +361,11 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
} }
}; };
const { ImageURLInput, UploadImageButton } = useAddImage({
handleUpdateImgList: handleUpdateImgList,
updateUidCount: updateUidCount
});
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleInputChange: handleInputChange handleInputChange: handleInputChange
})); }));
@@ -411,12 +419,10 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
{checkLabel} {checkLabel}
</Checkbox> </Checkbox>
)} )}
{actions.includes('upload') && message.role === Roles.User && ( {actions.includes('upload') &&
<UploadImg message.role === Roles.User &&
handleUpdateImgList={handleUpdateImgList} UploadImageButton}
size="middle"
></UploadImg>
)}
{actions.includes('upload') && message.role === Roles.User && ( {actions.includes('upload') && message.role === Roles.User && (
<UploadAudio <UploadAudio
maxFileSize={1024 * 1024} maxFileSize={1024 * 1024}
@@ -462,6 +468,7 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
))} ))}
</> </>
)} )}
{ImageURLInput}
</div> </div>
<div className="actions"> <div className="actions">
{actions.includes('add') && ( {actions.includes('add') && (
@@ -533,6 +540,7 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
</AudioWrapper> </AudioWrapper>
)} )}
</ImgsWrapper> </ImgsWrapper>
<div className="input-box"> <div className="input-box">
{actions.includes('paste') ? ( {actions.includes('paste') ? (
<TextArea <TextArea
@@ -12,6 +12,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import { Roles } from '../../config'; import { Roles } from '../../config';
@@ -20,7 +21,7 @@ import {
MessageItem, MessageItem,
MessageItemAction MessageItemAction
} from '../../config/types'; } from '../../config/types';
import UploadImg from '../upload-img'; import useAddImage from '../../hooks/use-add-image';
interface MessageActionsProps { interface MessageActionsProps {
data: MessageItem; data: MessageItem;
@@ -57,6 +58,7 @@ const MessageActions: React.FC<MessageActionsProps> = ({
file: any; file: any;
fileList: any[]; fileList: any[];
}) => { }) => {
try {
const base64Audio = await convertFileToBase64(audio.file); const base64Audio = await convertFileToBase64(audio.file);
const audioData = await readAudioFile(audio.file); const audioData = await readAudioFile(audio.file);
updateMessage?.({ updateMessage?.({
@@ -73,16 +75,30 @@ const MessageActions: React.FC<MessageActionsProps> = ({
} }
] ]
}); });
} catch (error) {
console.log('error uploading audio file', error);
}
}; };
const { ImageURLInput, UploadImageButton, isFromUrl } = useAddImage({
size: 'small',
handleUpdateImgList: handleUpdateImgList,
updateUidCount: () => `img-${Date.now()}`
});
return ( return (
<> <>
{ImageURLInput}
{actions.length > 1 && !loading ? ( {actions.length > 1 && !loading ? (
<div className="actions"> <div
className={classNames('actions', {
'has-url-input': isFromUrl
})}
>
<div className="actions-wrap gap-5"> <div className="actions-wrap gap-5">
{actions.includes('upload') && data.role === Roles.User && ( {actions.includes('upload') &&
<UploadImg handleUpdateImgList={handleUpdateImgList} /> data.role === Roles.User &&
)} UploadImageButton}
{actions.includes('upload') && data.role === Roles.User && ( {actions.includes('upload') && data.role === Roles.User && (
<UploadAudio <UploadAudio
type="text" type="text"
@@ -16,10 +16,8 @@ import ThumbImg from '../thumb-img';
import ThinkContent from './think-content'; import ThinkContent from './think-content';
const AudioWrapper = styled.div` const AudioWrapper = styled.div`
padding-top: 10px;
height: max-content; height: max-content;
width: max-content; width: max-content;
margin-inline: 10px;
`; `;
const ThumbImgWrapper = styled.div.attrs({ const ThumbImgWrapper = styled.div.attrs({
@@ -29,6 +27,12 @@ const ThumbImgWrapper = styled.div.attrs({
justify-content: flex-start; justify-content: flex-start;
overflow-x: auto; overflow-x: auto;
flex-direction: column; flex-direction: column;
padding: 0px;
gap: 8px;
&.has-content {
padding-inline: 8px;
padding-block: 8px 0;
}
`; `;
interface MessageBodyProps { 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) => { const handleClickWrapper = (e: any) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@@ -218,7 +197,7 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
data.imgs?.length || (data.audio && data.audio?.length > 0) data.imgs?.length || (data.audio && data.audio?.length > 0)
})} })}
> >
<div className="justify-start "> <div className="justify-start">
<ThumbImg <ThumbImg
editable={editable} editable={editable}
dataList={data.imgs || []} dataList={data.imgs || []}
@@ -249,7 +228,12 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
})} })}
onClick={handleClickWrapper} onClick={handleClickWrapper}
> >
<ThumbImgWrapper> <ThumbImgWrapper
className={classNames({
'has-content':
data.imgs?.length || (data.audio && data.audio?.length > 0)
})}
>
<ThumbImg <ThumbImg
style={{ paddingBlockEnd: 0 }} style={{ paddingBlockEnd: 0 }}
editable={editable} editable={editable}
@@ -257,7 +241,7 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
onDelete={handleDeleteImg} onDelete={handleDeleteImg}
/> />
{data.audio && data.audio.length > 0 && ( {data.audio && data.audio.length > 0 && (
<AudioWrapper className={data.imgs?.length ? '' : 'm-l-10'}> <AudioWrapper>
<SimpleAudio <SimpleAudio
url={data.audio?.[0]?.data.url} url={data.audio?.[0]?.data.url}
name={data.audio?.[0]?.data.name} 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;
+5 -1
View File
@@ -156,11 +156,15 @@ const Playground: React.FC = () => {
activeKey={activeKey} activeKey={activeKey}
key="view-code-buttons" key="view-code-buttons"
/>, />,
<div key="divider-wrapper">
{activeKey === 'chat' && (
<Divider <Divider
key="divider" key="divider"
type="vertical" type="vertical"
style={{ height: 24, marginInline: 16 }} style={{ height: 24, marginInline: 16 }}
/>, />
)}
</div>,
<ExtraContent key="extra-content" /> <ExtraContent key="extra-content" />
]} ]}
> >
@@ -33,6 +33,11 @@
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
background-color: var(--color-white-1); background-color: var(--color-white-1);
&.has-url-input {
display: flex;
margin-left: 8px;
}
} }
.actions-wrap { .actions-wrap {
@@ -58,5 +58,4 @@
display: flex; display: flex;
gap: 10px; gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
padding: 10px;
} }
+24 -19
View File
@@ -1,5 +1,4 @@
import DeleteModal from '@/components/delete-modal'; import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools'; import { FilterBar } from '@/components/page-tools';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import PageBox from '@/pages/_components/page-box'; import PageBox from '@/pages/_components/page-box';
@@ -11,11 +10,11 @@ import {
} from '@/pages/cluster-management/config'; } from '@/pages/cluster-management/config';
import { ClusterListItem } from '@/pages/cluster-management/config/types'; import { ClusterListItem } from '@/pages/cluster-management/config/types';
import useAddWorker from '@/pages/cluster-management/hooks/use-add-worker'; 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 { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, ConfigProvider, Table, message } from 'antd'; import { ConfigProvider, Table, message } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import NoResult from '../../_components/no-result';
import { import {
WORKERS_API, WORKERS_API,
deleteWorker, deleteWorker,
@@ -188,27 +187,33 @@ const Workers: React.FC = () => {
if (!currentData) { if (!currentData) {
currentData = clusterData.list[0]; currentData = clusterData.list[0];
} }
if (currentData) {
handleAddWorker(currentData as ClusterListItem); 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) => { const renderEmpty = (type?: string) => {
if (type !== 'Table') return; if (type !== 'Table') return;
return ( return noResourceResult;
<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>
);
}; };
const handleClusterChange = (value: number) => { const handleClusterChange = (value: number) => {
+25 -4
View File
@@ -57,7 +57,7 @@ export const GPUsConfigs: Record<
[GPUDriverMap.ILUVATAR]: { [GPUDriverMap.ILUVATAR]: {
label: ManufacturerMap[GPUDriverMap.ILUVATAR], label: ManufacturerMap[GPUDriverMap.ILUVATAR],
value: GPUDriverMap.ILUVATAR, value: GPUDriverMap.ILUVATAR,
runtime: 'iluvatar', // TODO: confirm runtime name runtime: 'iluvatar',
driver: 'ixsmi' driver: 'ixsmi'
}, },
[GPUDriverMap.CAMBRICON]: { [GPUDriverMap.CAMBRICON]: {
@@ -142,7 +142,7 @@ const setImageArgs = (params: any) => {
--token ${params.token} \\`; --token ${params.token} \\`;
}; };
// avaliable for NVIDIA、AMD、MThreads // avaliable for NVIDIA、MThreads
const registerWorker = (params: { const registerWorker = (params: {
server: string; server: string;
tag: string; tag: string;
@@ -162,6 +162,27 @@ const registerWorker = (params: {
${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''}`; ${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 // avaliable for Ascend
const registerAscendWorker = (params: { const registerAscendWorker = (params: {
server: string; server: string;
@@ -264,7 +285,7 @@ const registerCambriconWorker = (params: {
export const registerAddWokerCommandMap = { export const registerAddWokerCommandMap = {
[GPUDriverMap.NVIDIA]: registerWorker, [GPUDriverMap.NVIDIA]: registerWorker,
[GPUDriverMap.AMD]: registerWorker, [GPUDriverMap.AMD]: registerAMDWorker,
[GPUDriverMap.ASCEND]: registerAscendWorker, [GPUDriverMap.ASCEND]: registerAscendWorker,
[GPUDriverMap.HYGON]: registerHygonWorker, [GPUDriverMap.HYGON]: registerHygonWorker,
[GPUDriverMap.ILUVATAR]: registerIluvatarWorker, [GPUDriverMap.ILUVATAR]: registerIluvatarWorker,
@@ -275,7 +296,7 @@ export const registerAddWokerCommandMap = {
export const AddWorkerDockerNotes: Record<string, string[]> = { export const AddWorkerDockerNotes: Record<string, string[]> = {
[GPUDriverMap.NVIDIA]: [], [GPUDriverMap.NVIDIA]: [],
[GPUDriverMap.AMD]: [], [GPUDriverMap.AMD]: ['clusters.addworker.amdNotes-01'],
[GPUDriverMap.MOORE_THREADS]: [], [GPUDriverMap.MOORE_THREADS]: [],
[GPUDriverMap.ASCEND]: [], [GPUDriverMap.ASCEND]: [],
[GPUDriverMap.HYGON]: [ [GPUDriverMap.HYGON]: [
+4 -6
View File
@@ -6,7 +6,7 @@ import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import { useIntl, useModel } from '@umijs/max'; import { useIntl, useModel } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, ConfigProvider, message, Table } from 'antd'; import { ConfigProvider, message, Table } from 'antd';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import NoResult from '../_components/no-result'; import NoResult from '../_components/no-result';
import PageBox from '../_components/page-box'; import PageBox from '../_components/page-box';
@@ -150,11 +150,9 @@ const Users: React.FC = () => {
})} })}
title={intl.formatMessage({ id: 'noresult.users.title' })} title={intl.formatMessage({ id: 'noresult.users.title' })}
subTitle={intl.formatMessage({ id: 'noresult.users.subTitle' })} subTitle={intl.formatMessage({ id: 'noresult.users.subTitle' })}
> onClick={handleAddUser}
<Button type="primary" onClick={handleAddUser}> buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
{intl.formatMessage({ id: 'noresult.button.add' })} ></NoResult>
</Button>
</NoResult>
); );
}; };
-8
View File
@@ -28,7 +28,6 @@ export const loadAudioData = async (
url: string; url: string;
}> => { }> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try {
const audioBlob = new Blob([data], { type: type }); const audioBlob = new Blob([data], { type: type });
const fileSize = convertFileSize(audioBlob.size); const fileSize = convertFileSize(audioBlob.size);
@@ -56,22 +55,16 @@ export const loadAudioData = async (
message.error('Failed to load audio metadata invalid file'); message.error('Failed to load audio metadata invalid file');
reject(new 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);
}
}); });
}; };
export const readAudioFile = async ( export const readAudioFile = async (
file: File file: File
): Promise<{ url: string; name: string; duration: number }> => { ): Promise<{ url: string; name: string; duration: number }> => {
console.log('file====', file);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async function (e: any) { reader.onload = async function (e: any) {
try { try {
console.log('file====', file);
const arrayBuffer = e.target.result; const arrayBuffer = e.target.result;
const audioData = await loadAudioData(arrayBuffer, file.type); const audioData = await loadAudioData(arrayBuffer, file.type);
resolve({ resolve({
@@ -79,7 +72,6 @@ export const readAudioFile = async (
name: file.name name: file.name
}); });
} catch (error) { } catch (error) {
console.log('error====', error);
reject(error); reject(error);
} }
}; };