chore: playground mutilmodel input
This commit is contained in:
@@ -31,6 +31,13 @@ export default [
|
|||||||
access: 'canSeeAdmin',
|
access: 'canSeeAdmin',
|
||||||
component: './resources'
|
component: './resources'
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'usage',
|
||||||
|
// path: '/usage',
|
||||||
|
// key: 'usage',
|
||||||
|
// icon: 'BarChartOutlined',
|
||||||
|
// component: './usage'
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
name: 'apikeys',
|
name: 'apikeys',
|
||||||
path: '/api-keys',
|
path: '/api-keys',
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const Chart: React.FC<{
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
};
|
};
|
||||||
}, [resize, chart]);
|
}, [resize]);
|
||||||
|
|
||||||
return <div ref={container} style={{ width: width, height }}></div>;
|
return <div ref={container} style={{ width: width, height }}></div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ const Header: React.FC<HeaderProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default React.memo(Header);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type PaginationProps
|
type PaginationProps
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import Header from './components/header';
|
import Header from './components/header';
|
||||||
import TableRow from './components/table-row';
|
import TableRow from './components/table-row';
|
||||||
import './styles/index.less';
|
import './styles/index.less';
|
||||||
@@ -75,7 +75,7 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
|
|||||||
pagination?.onShowSizeChange?.(current, size);
|
pagination?.onShowSizeChange?.(current, size);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderHeaderPrefix = () => {
|
const renderHeaderPrefix = useMemo(() => {
|
||||||
if (expandable && rowSelection) {
|
if (expandable && rowSelection) {
|
||||||
return (
|
return (
|
||||||
<div className="header-row-prefix-wrapper">
|
<div className="header-row-prefix-wrapper">
|
||||||
@@ -107,9 +107,9 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
}, [expandable, rowSelection, selectAll, indeterminate]);
|
||||||
|
|
||||||
const renderContent = useCallback(() => {
|
const renderContent = useMemo(() => {
|
||||||
if (!props.dataSource.length) {
|
if (!props.dataSource.length) {
|
||||||
return (
|
return (
|
||||||
<div className="empty-wrapper">
|
<div className="empty-wrapper">
|
||||||
@@ -148,12 +148,12 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
|
|||||||
<div className="seal-table-container">
|
<div className="seal-table-container">
|
||||||
{
|
{
|
||||||
<div className="header-row-wrapper">
|
<div className="header-row-wrapper">
|
||||||
{renderHeaderPrefix()}
|
{renderHeaderPrefix}
|
||||||
<Header onSort={onSort}>{children}</Header>
|
<Header onSort={onSort}>{children}</Header>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Spin spinning={loading}>{renderContent()}</Spin>
|
<Spin spinning={loading}>{renderContent}</Spin>
|
||||||
</div>
|
</div>
|
||||||
{pagination && (
|
{pagination && (
|
||||||
<div className="pagination-wrapper">
|
<div className="pagination-wrapper">
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
:local(.status-content-wrapper) {
|
||||||
|
// padding-top: 24px;
|
||||||
|
&:hover {
|
||||||
|
:global(.copy-button-wrapper) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.copy-button-wrapper {
|
||||||
|
display: none;
|
||||||
|
background-color: #383838;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
border-radius: 0 8px 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import { StatusType } from '@/config/types';
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import CopyButton from '../copy-button';
|
||||||
|
import CopyStyle from './copy.less';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
export const StatusMaps = {
|
export const StatusMaps = {
|
||||||
@@ -58,6 +60,20 @@ const StatusTag: React.FC<StatusTagProps> = ({
|
|||||||
}
|
}
|
||||||
return <span>{text}</span>;
|
return <span>{text}</span>;
|
||||||
};
|
};
|
||||||
|
const renderTitle = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<div className={CopyStyle['status-content-wrapper']}>
|
||||||
|
<div className="copy-button-wrapper">
|
||||||
|
<CopyButton
|
||||||
|
style={{ color: 'rgba(255,255,255,.8)' }}
|
||||||
|
text={statusValue.message || ''}
|
||||||
|
size="small"
|
||||||
|
></CopyButton>
|
||||||
|
</div>
|
||||||
|
<div>{statusValue.message}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [statusValue]);
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={classNames('status-tag', {
|
className={classNames('status-tag', {
|
||||||
@@ -70,7 +86,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
|
|||||||
>
|
>
|
||||||
{statusValue.message ? (
|
{statusValue.message ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={statusValue.message}
|
title={renderTitle}
|
||||||
overlayInnerStyle={{ maxHeight: 200, overflow: 'auto' }}
|
overlayInnerStyle={{ maxHeight: 200, overflow: 'auto' }}
|
||||||
>
|
>
|
||||||
<span className="m-r-5">
|
<span className="m-r-5">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import menu from './en-US/menu';
|
|||||||
import models from './en-US/models';
|
import models from './en-US/models';
|
||||||
import playground from './en-US/playground';
|
import playground from './en-US/playground';
|
||||||
import resources from './en-US/resources';
|
import resources from './en-US/resources';
|
||||||
|
import usage from './en-US/usage';
|
||||||
import users from './en-US/users';
|
import users from './en-US/users';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -15,5 +16,6 @@ export default {
|
|||||||
...resources,
|
...resources,
|
||||||
...apikeys,
|
...apikeys,
|
||||||
...users,
|
...users,
|
||||||
...dashboard
|
...dashboard,
|
||||||
|
...usage
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export default {
|
|||||||
'common.button.editmode': 'Edit mode',
|
'common.button.editmode': 'Edit mode',
|
||||||
'common.button.add': 'Add',
|
'common.button.add': 'Add',
|
||||||
'common.button.login': 'Log In',
|
'common.button.login': 'Log In',
|
||||||
|
'common.button.select': 'Select',
|
||||||
|
'common.button.selected': 'Selected',
|
||||||
'common.button.continue': 'Continue',
|
'common.button.continue': 'Continue',
|
||||||
'common.button.upload': 'Upload',
|
'common.button.upload': 'Upload',
|
||||||
'common.button.save': 'Save',
|
'common.button.save': 'Save',
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ export default {
|
|||||||
'menu.apikeys': 'API Keys',
|
'menu.apikeys': 'API Keys',
|
||||||
'menu.users': 'Users',
|
'menu.users': 'Users',
|
||||||
'menu.profile': 'Profile',
|
'menu.profile': 'Profile',
|
||||||
'menu.login': 'Login'
|
'menu.login': 'Login',
|
||||||
|
'menu.usage': 'Usage'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
'models.button.deploy': 'Deploy Model From',
|
'models.button.deploy': 'Deploy Model',
|
||||||
'models.title': 'Models',
|
'models.title': 'Models',
|
||||||
'models.title.edit': 'Edit Model',
|
'models.title.edit': 'Edit Model',
|
||||||
'models.table.models': 'models',
|
'models.table.models': 'models',
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
'usage.title': 'Usage',
|
||||||
|
'usage.filter.user': 'Filter by User',
|
||||||
|
'usage.filter.model': 'Filter by Model'
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
|
|
||||||
export default {
|
const langConfigMap = {
|
||||||
'ar-EG': {
|
'ar-EG': {
|
||||||
lang: 'ar-EG',
|
lang: 'ar-EG',
|
||||||
label: 'العربية',
|
label: 'العربية',
|
||||||
@@ -326,3 +326,7 @@ export default {
|
|||||||
title: '語言'
|
title: '語言'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LangConfigType = keyof typeof langConfigMap;
|
||||||
|
|
||||||
|
export default langConfigMap;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import menu from './zh-CN/menu';
|
|||||||
import models from './zh-CN/models';
|
import models from './zh-CN/models';
|
||||||
import playground from './zh-CN/playground';
|
import playground from './zh-CN/playground';
|
||||||
import resources from './zh-CN/resources';
|
import resources from './zh-CN/resources';
|
||||||
|
import usage from './zh-CN/usage';
|
||||||
import users from './zh-CN/users';
|
import users from './zh-CN/users';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -15,5 +16,6 @@ export default {
|
|||||||
...resources,
|
...resources,
|
||||||
...apikeys,
|
...apikeys,
|
||||||
...users,
|
...users,
|
||||||
...dashboard
|
...dashboard,
|
||||||
|
...usage
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export default {
|
|||||||
'common.button.editmode': '编辑模式',
|
'common.button.editmode': '编辑模式',
|
||||||
'common.button.add': '添加',
|
'common.button.add': '添加',
|
||||||
'common.button.login': '登录',
|
'common.button.login': '登录',
|
||||||
|
'common.button.select': '选择',
|
||||||
|
'common.button.selected': '已选择',
|
||||||
'common.button.continue': '继续',
|
'common.button.continue': '继续',
|
||||||
'common.button.upload': '上传',
|
'common.button.upload': '上传',
|
||||||
'common.button.save': '保存',
|
'common.button.save': '保存',
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ export default {
|
|||||||
'menu.apikeys': 'API 密钥',
|
'menu.apikeys': 'API 密钥',
|
||||||
'menu.users': '用户',
|
'menu.users': '用户',
|
||||||
'menu.profile': '个人信息',
|
'menu.profile': '个人信息',
|
||||||
'menu.login': '登录'
|
'menu.login': '登录',
|
||||||
|
'menu.usage': '使用量'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
'usage.title': '使用量',
|
||||||
|
'usage.filter.user': '按用户查询',
|
||||||
|
'usage.filter.model': '按模型查询'
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ import LineChart from '@/components/echarts/line-chart';
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo, useContext } from 'react';
|
import { memo, useContext, useMemo } from 'react';
|
||||||
import { DashboardContext } from '../config/dashboard-context';
|
import { DashboardContext } from '../config/dashboard-context';
|
||||||
|
|
||||||
const chartColorMap = {
|
const chartColorMap = {
|
||||||
@@ -119,7 +119,7 @@ const UtilizationOvertime: React.FC = () => {
|
|||||||
const tooltipValueFormatter = (value: any) => {
|
const tooltipValueFormatter = (value: any) => {
|
||||||
return !value ? value : `${value}%`;
|
return !value ? value : `${value}%`;
|
||||||
};
|
};
|
||||||
const generateData = () => {
|
const generateData = useMemo(() => {
|
||||||
const legendData: string[] = [];
|
const legendData: string[] = [];
|
||||||
const xAxisData: string[] = [];
|
const xAxisData: string[] = [];
|
||||||
let seriesData: { value: number; time: string; type: string }[] = [];
|
let seriesData: { value: number; time: string; type: string }[] = [];
|
||||||
@@ -147,16 +147,15 @@ const UtilizationOvertime: React.FC = () => {
|
|||||||
legendData,
|
legendData,
|
||||||
xAxisData: _.uniq(xAxisData)
|
xAxisData: _.uniq(xAxisData)
|
||||||
};
|
};
|
||||||
};
|
}, [data]);
|
||||||
const { seriesData, legendData, xAxisData } = generateData();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LineChart
|
<LineChart
|
||||||
height={390}
|
height={390}
|
||||||
seriesData={seriesData}
|
seriesData={generateData.seriesData}
|
||||||
legendData={legendData}
|
legendData={generateData.legendData}
|
||||||
xAxisData={xAxisData}
|
xAxisData={generateData.xAxisData}
|
||||||
tooltipValueFormatter={tooltipValueFormatter}
|
tooltipValueFormatter={tooltipValueFormatter}
|
||||||
smooth={true}
|
smooth={true}
|
||||||
width="100%"
|
width="100%"
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export async function queryHuggingfaceModels(
|
|||||||
additionalFields: ['sha'],
|
additionalFields: ['sha'],
|
||||||
fetch(url: string, config: any) {
|
fetch(url: string, config: any) {
|
||||||
try {
|
try {
|
||||||
return fetch(`${url}`, {
|
return fetch(url, {
|
||||||
...config,
|
...config,
|
||||||
signal: options.signal
|
signal: options.signal
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -308,16 +308,22 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
borderRadius: '8px 0 0 8px'
|
borderRadius: '8px 0 0 8px'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
width="90%"
|
width={
|
||||||
|
modelSource === modelSourceMap.huggingface_value
|
||||||
|
? 'calc(100vw - 220px)'
|
||||||
|
: 600
|
||||||
|
}
|
||||||
footer={false}
|
footer={false}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<ColumnWrapper>
|
{modelSource === modelSourceMap.huggingface_value && (
|
||||||
<SearchModel
|
<ColumnWrapper>
|
||||||
modelSource={modelSource}
|
<SearchModel
|
||||||
onSelectModel={handleOnSelectModel}
|
modelSource={modelSource}
|
||||||
></SearchModel>
|
onSelectModel={handleOnSelectModel}
|
||||||
</ColumnWrapper>
|
></SearchModel>
|
||||||
|
</ColumnWrapper>
|
||||||
|
)}
|
||||||
{modelSource === modelSourceMap.huggingface_value && (
|
{modelSource === modelSourceMap.huggingface_value && (
|
||||||
<ColumnWrapper>
|
<ColumnWrapper>
|
||||||
<ModelCard repo={huggingfaceRepoId}></ModelCard>
|
<ModelCard repo={huggingfaceRepoId}></ModelCard>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { convertFileSize } from '@/utils';
|
import { convertFileSize } from '@/utils';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { Button, Col, Empty, Row, Space, Spin } from 'antd';
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Col, Empty, Row, Space, Spin } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -14,6 +15,7 @@ interface HFModelFileProps {
|
|||||||
onSelectFile?: (file: any) => void;
|
onSelectFile?: (file: any) => void;
|
||||||
}
|
}
|
||||||
const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [dataSource, setDataSource] = useState<any>({
|
const [dataSource, setDataSource] = useState<any>({
|
||||||
fileList: [],
|
fileList: [],
|
||||||
loading: false
|
loading: false
|
||||||
@@ -61,6 +63,13 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnEnter = (e: any, item: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSelectModelFile(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleFetchModelFiles();
|
handleFetchModelFiles();
|
||||||
}, [props.repo]);
|
}, [props.repo]);
|
||||||
@@ -72,16 +81,17 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
|||||||
<div style={{ padding: '16px 20px' }}>
|
<div style={{ padding: '16px 20px' }}>
|
||||||
<Spin spinning={dataSource.loading} style={{ minHeight: 100 }}>
|
<Spin spinning={dataSource.loading} style={{ minHeight: 100 }}>
|
||||||
{dataSource.fileList.length ? (
|
{dataSource.fileList.length ? (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 24]}>
|
||||||
{_.map(dataSource.fileList, (item: any) => {
|
{_.map(dataSource.fileList, (item: any) => {
|
||||||
return (
|
return (
|
||||||
<Col span={24} key={item.path}>
|
<Col span={24} key={item.path}>
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
|
||||||
className={classNames('hf-model-file', {
|
className={classNames('hf-model-file', {
|
||||||
active: item.path === current
|
active: item.path === current
|
||||||
})}
|
})}
|
||||||
|
tabIndex={0}
|
||||||
onClick={() => handleSelectModelFile(item)}
|
onClick={() => handleSelectModelFile(item)}
|
||||||
|
onKeyDown={(e) => handleOnEnter(e, item)}
|
||||||
>
|
>
|
||||||
<div className="title">{item.path}</div>
|
<div className="title">{item.path}</div>
|
||||||
<Space className="tags">
|
<Space className="tags">
|
||||||
@@ -91,9 +101,15 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
|||||||
{getModelQuantizationType(item)}
|
{getModelQuantizationType(item)}
|
||||||
</Space>
|
</Space>
|
||||||
<div className="btn">
|
<div className="btn">
|
||||||
<Button size="middle">
|
{/* <Button size="middle">
|
||||||
{item.path === current ? 'Selected' : 'Select'}
|
{item.path === current
|
||||||
</Button>
|
? intl.formatMessage({
|
||||||
|
id: 'common.button.selected'
|
||||||
|
})
|
||||||
|
: intl.formatMessage({
|
||||||
|
id: 'common.button.select'
|
||||||
|
})}
|
||||||
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
HeartOutlined
|
HeartOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Button, Space, Tag } from 'antd';
|
import { Space, Tag } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -42,14 +42,12 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
|
|||||||
<Space size={16}>
|
<Space size={16}>
|
||||||
{props.task && (
|
{props.task && (
|
||||||
<Tag
|
<Tag
|
||||||
|
color="geekblue"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-white-1)',
|
|
||||||
marginRight: 0
|
marginRight: 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
<span style={{ opacity: 0.65 }}>{props.task}</span>
|
||||||
{props.task}
|
|
||||||
</span>
|
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
@@ -86,9 +84,9 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
|
|||||||
})}
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
<div className="btn">
|
<div className="btn">
|
||||||
<Button size="middle">
|
{/* <Button size="middle">
|
||||||
{props.active ? 'Selected' : 'Select'}
|
{props.active ? 'Selected' : 'Select'}
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
|
import { downloadFile } from '@huggingface/hub';
|
||||||
import { Button, Empty, Tag } from 'antd';
|
import { Button, Empty, Tag } from 'antd';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { queryHuggingfaceModelDetail } from '../apis';
|
import { queryHuggingfaceModelDetail } from '../apis';
|
||||||
@@ -9,6 +10,13 @@ const ModelCard: React.FC<{ repo: string }> = (props) => {
|
|||||||
const { repo } = props;
|
const { repo } = props;
|
||||||
const [modelData, setModelData] = useState<any>({});
|
const [modelData, setModelData] = useState<any>({});
|
||||||
|
|
||||||
|
const loadFile = async (repo: string, sha: string) => {
|
||||||
|
const res = await (
|
||||||
|
await downloadFile({ repo, revision: sha, path: 'README.md' })
|
||||||
|
)?.text();
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
const getModelCardData = async () => {
|
const getModelCardData = async () => {
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
setModelData(null);
|
setModelData(null);
|
||||||
@@ -16,6 +24,12 @@ const ModelCard: React.FC<{ repo: string }> = (props) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await queryHuggingfaceModelDetail({ repo });
|
const res = await queryHuggingfaceModelDetail({ repo });
|
||||||
|
const modelConfig = await loadFile(repo, res.sha);
|
||||||
|
console.log('modelcard=======', {
|
||||||
|
res,
|
||||||
|
repo,
|
||||||
|
modelConfig
|
||||||
|
});
|
||||||
setModelData(res);
|
setModelData(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setModelData({});
|
setModelData({});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
|
import { BulbOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Input, Select } from 'antd';
|
import { Button, Input, Select } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -243,6 +244,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
|
|||||||
renderHFSearch()
|
renderHFSearch()
|
||||||
) : (
|
) : (
|
||||||
<div style={{ lineHeight: '18px' }}>
|
<div style={{ lineHeight: '18px' }}>
|
||||||
|
<BulbOutlined className="font-size-14 m-r-5" />
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: 'model.form.ollamatips' },
|
{ id: 'model.form.ollamatips' },
|
||||||
{ name: intl.formatMessage({ id: 'model.form.ollama.model' }) }
|
{ name: intl.formatMessage({ id: 'model.form.ollama.model' }) }
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelect?.(item);
|
onSelect?.(item);
|
||||||
};
|
};
|
||||||
|
const handleOnEnter = (e: any, item: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onSelect?.(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div style={{ ...props.style }} className="search-result-wrap">
|
<div style={{ ...props.style }} className="search-result-wrap">
|
||||||
<Spin spinning={props.loading}>
|
<Spin spinning={props.loading}>
|
||||||
@@ -29,7 +35,10 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
|
|||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{resultList.map((item, index) => (
|
{resultList.map((item, index) => (
|
||||||
<Col span={24} key={item.name}>
|
<Col span={24} key={item.name}>
|
||||||
<div onClick={(e) => handleSelect(e, item)}>
|
<div
|
||||||
|
onClick={(e) => handleSelect(e, item)}
|
||||||
|
onKeyDown={(e) => handleOnEnter(e, item)}
|
||||||
|
>
|
||||||
<HFModelItem
|
<HFModelItem
|
||||||
source={source}
|
source={source}
|
||||||
tags={item.tags}
|
tags={item.tags}
|
||||||
|
|||||||
@@ -4,6 +4,19 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
border-left: 1px solid var(--ant-color-split);
|
border-left: 1px solid var(--ant-color-split);
|
||||||
|
// custom scrollbar
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--ant-color-fill);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: var(--color-white-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-wrapper-footer {
|
.column-wrapper-footer {
|
||||||
|
|||||||
@@ -3,18 +3,28 @@
|
|||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 194px);
|
||||||
|
// custom scrollbar
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--ant-color-fill);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: var(--color-white-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding-inline: 20px;
|
padding-inline: 20px;
|
||||||
background: var(--color-white-1);
|
background: var(--color-white-1);
|
||||||
border-bottom: 1px solid var(--ant-color-split);
|
border-bottom: 1px solid var(--ant-color-split);
|
||||||
// box-shadow: 0 1px 2px rgba(5, 5, 5, 5%);
|
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
|
|
||||||
.filter {
|
.filter {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import HotKeys from '@/config/hotkeys';
|
|||||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Input, Space, Tooltip } from 'antd';
|
import { Button, Input, Space, Tooltip } from 'antd';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import _ from 'lodash';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { Roles } from '../config';
|
import { Roles } from '../config';
|
||||||
import '../style/message-item.less';
|
import '../style/message-item.less';
|
||||||
|
import ThumbImg from './thumb-img';
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
role: string;
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
@@ -26,6 +28,11 @@ const MessageItem: React.FC<{
|
|||||||
const [isTyping, setIsTyping] = useState(false);
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
const [currentIsFocus, setCurrentIsFocus] = useState(isFocus);
|
const [currentIsFocus, setCurrentIsFocus] = useState(isFocus);
|
||||||
|
const [imgList, setImgList] = useState<{ uid: number; dataUrl: string }[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const imgCountRef = useRef(0);
|
||||||
|
|
||||||
const inputRef = useRef<any>(null);
|
const inputRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,6 +68,63 @@ const MessageItem: React.FC<{
|
|||||||
uid: message.uid
|
uid: message.uid
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPasteContent = useCallback(async (event: any) => {
|
||||||
|
const clipboardData = event.clipboardData || window.clipboardData;
|
||||||
|
const items = clipboardData.items;
|
||||||
|
const imgPromises: Promise<string>[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
let item = items[i];
|
||||||
|
console.log('item===========', item);
|
||||||
|
|
||||||
|
if (item.kind === 'file' && item.type.indexOf('image') !== -1) {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
const imgPromise = new Promise<string>((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function (event) {
|
||||||
|
const base64String = event.target?.result as string;
|
||||||
|
if (base64String) {
|
||||||
|
resolve(base64String);
|
||||||
|
} else {
|
||||||
|
reject('Failed to convert image to base64');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
imgPromises.push(imgPromise);
|
||||||
|
} else if (item.kind === 'string') {
|
||||||
|
// string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imgs = await Promise.all(imgPromises);
|
||||||
|
if (imgs.length) {
|
||||||
|
const list = _.map(imgs, (img: string) => {
|
||||||
|
imgCountRef.current += 1;
|
||||||
|
return {
|
||||||
|
uid: imgCountRef.current,
|
||||||
|
dataUrl: img
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setImgList((pre) => {
|
||||||
|
return [...pre, ...list];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing images:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteImg = useCallback(
|
||||||
|
(uid: number) => {
|
||||||
|
const list = imgList.filter((item) => item.uid !== uid);
|
||||||
|
setImgList(list);
|
||||||
|
},
|
||||||
|
[imgList]
|
||||||
|
);
|
||||||
|
|
||||||
const handleMessageChange = (e: any) => {
|
const handleMessageChange = (e: any) => {
|
||||||
// setIsTyping(true);
|
// setIsTyping(true);
|
||||||
handleUpdateMessage({ role: message.role, message: e.target.value });
|
handleUpdateMessage({ role: message.role, message: e.target.value });
|
||||||
@@ -86,6 +150,16 @@ const MessageItem: React.FC<{
|
|||||||
onDelete();
|
onDelete();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnPaste = (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const text = e.clipboardData.getData('text');
|
||||||
|
if (text) {
|
||||||
|
handleUpdateMessage({ role: message.role, message: text });
|
||||||
|
} else {
|
||||||
|
getPasteContent(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
HotKeys.SUBMIT,
|
HotKeys.SUBMIT,
|
||||||
() => {
|
() => {
|
||||||
@@ -107,6 +181,7 @@ const MessageItem: React.FC<{
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="message-content-input">
|
<div className="message-content-input">
|
||||||
|
<ThumbImg dataList={imgList} onDelete={handleDeleteImg}></ThumbImg>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={{ paddingBlock: '12px' }}
|
style={{ paddingBlock: '12px' }}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Space } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import '../style/thumb-img.less';
|
||||||
|
|
||||||
|
const ThumbImg: React.FC<{
|
||||||
|
dataList: any[];
|
||||||
|
onDelete: (uid: number) => void;
|
||||||
|
}> = ({ dataList, onDelete }) => {
|
||||||
|
const handleOnDelete = (uid: number) => {
|
||||||
|
onDelete(uid);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space wrap size={10} className="thumb-list-wrap">
|
||||||
|
{_.map(dataList, (item: any) => {
|
||||||
|
return (
|
||||||
|
<span key={item.uid} className="thumb-img">
|
||||||
|
<span
|
||||||
|
style={{ backgroundImage: `url(${item.dataUrl})` }}
|
||||||
|
className="img"
|
||||||
|
></span>
|
||||||
|
<span className="del" onClick={() => handleOnDelete(item.uid)}>
|
||||||
|
<CloseCircleOutlined />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThumbImg;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { useIntl, useSearchParams } from '@umijs/max';
|
import { useIntl, useSearchParams } from '@umijs/max';
|
||||||
import { Button, Divider } from 'antd';
|
import { Button, Divider, Space } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import GroundLeft from './components/ground-left';
|
import GroundLeft from './components/ground-left';
|
||||||
@@ -24,7 +24,7 @@ const Playground: React.FC = () => {
|
|||||||
<PageContainer
|
<PageContainer
|
||||||
ghost
|
ghost
|
||||||
extra={[
|
extra={[
|
||||||
<>
|
<Space key="buttons">
|
||||||
<Button
|
<Button
|
||||||
size="middle"
|
size="middle"
|
||||||
onClick={handleViewCode}
|
onClick={handleViewCode}
|
||||||
@@ -45,7 +45,7 @@ const Playground: React.FC = () => {
|
|||||||
></IconFont>
|
></IconFont>
|
||||||
}
|
}
|
||||||
></Button>
|
></Button>
|
||||||
</>
|
</Space>
|
||||||
]}
|
]}
|
||||||
className="playground-container"
|
className="playground-container"
|
||||||
>
|
>
|
||||||
@@ -58,14 +58,6 @@ const Playground: React.FC = () => {
|
|||||||
collapse: collapse
|
collapse: collapse
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* <Button
|
|
||||||
onClick={() => setCollapse(!collapse)}
|
|
||||||
icon={collapse ? <MenuFoldOutlined /> : <MenuUnfoldOutlined />}
|
|
||||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
className="collapse-btn"
|
|
||||||
></Button> */}
|
|
||||||
<div
|
<div
|
||||||
className={classNames('divider-line', {
|
className={classNames('divider-line', {
|
||||||
collapse: collapse
|
collapse: collapse
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
.thumb-img {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
cursor: pointer;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -2px;
|
||||||
|
font-size: var(--font-size-middle);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-white-1);
|
||||||
|
display: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.del {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-list-wrap {
|
||||||
|
padding: 10px;
|
||||||
|
// background-color: var(--color-fill-1);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Input, Space, Table, Tooltip } from 'antd';
|
import { Button, Input, Space, Table, Tooltip } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo, useEffect, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { deleteWorker, queryWorkersList } from '../apis';
|
import { deleteWorker, queryWorkersList } from '../apis';
|
||||||
import { WorkerStatusMapValue, status } from '../config';
|
import { WorkerStatusMapValue, status } from '../config';
|
||||||
import { Filesystem, GPUDeviceItem, ListItem } from '../config/types';
|
import { Filesystem, GPUDeviceItem, ListItem } from '../config/types';
|
||||||
@@ -46,7 +46,7 @@ const Resources: React.FC = () => {
|
|||||||
search: ''
|
search: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setDataSource((pre) => {
|
setDataSource((pre) => {
|
||||||
pre.loading = true;
|
pre.loading = true;
|
||||||
return { ...pre };
|
return { ...pre };
|
||||||
@@ -70,7 +70,7 @@ const Resources: React.FC = () => {
|
|||||||
});
|
});
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
}
|
}
|
||||||
};
|
}, [queryParams]);
|
||||||
|
|
||||||
const handlePageChange = (page: number, perPage: number | undefined) => {
|
const handlePageChange = (page: number, perPage: number | undefined) => {
|
||||||
console.log(page, perPage);
|
console.log(page, perPage);
|
||||||
@@ -81,9 +81,12 @@ const Resources: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
const handleTableChange = useCallback(
|
||||||
setSortOrder(sorter.order);
|
(pagination: any, filters: any, sorter: any) => {
|
||||||
};
|
setSortOrder(sorter.order);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSearch = (e: any) => {
|
const handleSearch = (e: any) => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -207,6 +210,7 @@ const Resources: React.FC = () => {
|
|||||||
}
|
}
|
||||||
></PageTools>
|
></PageTools>
|
||||||
<Table
|
<Table
|
||||||
|
style={{ width: '100%' }}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
|
export const DASHBOARD_API = '/dashboard';
|
||||||
|
|
||||||
|
export async function queryDashboardData() {
|
||||||
|
return request(DASHBOARD_API);
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import PageTools from '@/components/page-tools';
|
||||||
|
import { convertFileSize } from '@/utils';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Col, Row, Table } from 'antd';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { DashboardContext } from '../config/dashboard-context';
|
||||||
|
|
||||||
|
const projectColumns = [
|
||||||
|
{
|
||||||
|
title: 'Name',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Token Quota',
|
||||||
|
dataIndex: 'quota',
|
||||||
|
key: 'quota',
|
||||||
|
render: (text: any, record: any) => <span>{record.quota}k</span>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Token Utilization',
|
||||||
|
dataIndex: 'utilization',
|
||||||
|
key: 'utilization',
|
||||||
|
render: (text: any, record: any) => <span>{record.utilization}%</span>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Members',
|
||||||
|
dataIndex: 'members',
|
||||||
|
key: 'members'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const projectData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'copilot-dev',
|
||||||
|
quota: 100,
|
||||||
|
utilization: 50,
|
||||||
|
members: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'rag-wiki',
|
||||||
|
quota: 200,
|
||||||
|
utilization: 70,
|
||||||
|
members: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'smart-auto-agent',
|
||||||
|
quota: 100,
|
||||||
|
utilization: 20,
|
||||||
|
members: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'office-auto-docs',
|
||||||
|
quota: 100,
|
||||||
|
utilization: 25,
|
||||||
|
members: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'smart-customer-service',
|
||||||
|
quota: 100,
|
||||||
|
utilization: 46,
|
||||||
|
members: 2
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const ActiveTable = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const data = useContext(DashboardContext).active_models || [];
|
||||||
|
const modelColumns = [
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name'
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: intl.formatMessage({ id: 'dashboard.gpuutilization' }),
|
||||||
|
// dataIndex: 'gpu_utilization',
|
||||||
|
// key: 'gpu_utilization',
|
||||||
|
// render: (text: any, record: any) => (
|
||||||
|
// <ProgressBar percent={_.round(text, 0)}></ProgressBar>
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'dashboard.allocatevram' }),
|
||||||
|
dataIndex: 'resource_claim.memory',
|
||||||
|
key: 'gpu_memory',
|
||||||
|
render: (text: any, record: any) => {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{convertFileSize(record.resource_claim?.gpu_memory || 0)} /{' '}
|
||||||
|
{convertFileSize(record.resource_claim?.memory || 0)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'models.form.replicas' }),
|
||||||
|
dataIndex: 'instance_count',
|
||||||
|
key: 'instance_count'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'dashboard.tokens' }),
|
||||||
|
dataIndex: 'token_count',
|
||||||
|
key: 'token_count'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Row gutter={[20, 0]}>
|
||||||
|
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||||
|
<PageTools
|
||||||
|
style={{ margin: '26px 0px' }}
|
||||||
|
left={
|
||||||
|
<span style={{ padding: '9px 0' }}>
|
||||||
|
{intl.formatMessage({ id: 'dashboard.activeModels' })}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
right={false}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={modelColumns}
|
||||||
|
dataSource={data}
|
||||||
|
pagination={false}
|
||||||
|
rowKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActiveTable;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import PageTools from '@/components/page-tools';
|
||||||
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, Input, Select, Space } from 'antd';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { queryDashboardData } from '../apis';
|
||||||
|
import DashboardContext from '../config/dashboard-context';
|
||||||
|
import { DashboardProps } from '../config/types';
|
||||||
|
import ActiveTable from './active-table';
|
||||||
|
import Overview from './over-view';
|
||||||
|
import Usage from './usage';
|
||||||
|
|
||||||
|
const Page: React.FC<{ setLoading: (loading: boolean) => void }> = ({
|
||||||
|
setLoading
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [data, setData] = useState<DashboardProps>({} as DashboardProps);
|
||||||
|
|
||||||
|
const getDashboardData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await queryDashboardData();
|
||||||
|
setData(res);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
setData({} as DashboardProps);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
getDashboardData();
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<DashboardContext.Provider value={{ ...data, fetchData: getDashboardData }}>
|
||||||
|
<Overview></Overview>
|
||||||
|
{/* <SystemLoad></SystemLoad> */}
|
||||||
|
<PageTools
|
||||||
|
left={
|
||||||
|
<Space>
|
||||||
|
<Input
|
||||||
|
placeholder={intl.formatMessage({ id: 'usage.filter.user' })}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
allowClear
|
||||||
|
></Input>
|
||||||
|
<Select
|
||||||
|
style={{ width: 300 }}
|
||||||
|
placeholder={intl.formatMessage({ id: 'usage.filter.model' })}
|
||||||
|
></Select>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||||
|
icon={<SyncOutlined></SyncOutlined>}
|
||||||
|
></Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
></PageTools>
|
||||||
|
<Usage></Usage>
|
||||||
|
<ActiveTable></ActiveTable>
|
||||||
|
</DashboardContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Page);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
:local(.card-body) {
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
:global(.ant-card-body) {
|
||||||
|
height: 110px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
border-radius: var(--ant-border-radius-lg);
|
||||||
|
border: 1px solid var(--color-border-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:local(.row) {
|
||||||
|
:global(.ant-col-5) {
|
||||||
|
flex: 0 0 20%;
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Card, Col, Row, Space } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { overviewConfigs } from '../config';
|
||||||
|
import { DashboardContext } from '../config/dashboard-context';
|
||||||
|
import '../styles/index.less';
|
||||||
|
import styles from './over-view.less';
|
||||||
|
|
||||||
|
const renderCardItem = (data: {
|
||||||
|
label: string;
|
||||||
|
value: React.ReactNode;
|
||||||
|
bgColor: string;
|
||||||
|
}) => {
|
||||||
|
const { label, value, bgColor } = data;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
bordered={false}
|
||||||
|
style={{ background: bgColor }}
|
||||||
|
className={styles['card-body']}
|
||||||
|
>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className="label">{label}</div>
|
||||||
|
<div className="value">{value}</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const Overview: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const data = useContext(DashboardContext).resource_counts || {};
|
||||||
|
console.log('overview===');
|
||||||
|
const renderValue = (
|
||||||
|
value:
|
||||||
|
| number
|
||||||
|
| {
|
||||||
|
healthy: number;
|
||||||
|
warning: number;
|
||||||
|
error: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Space className="value-box" size={20}>
|
||||||
|
<span className={'value-healthy'}>{value.healthy}</span>
|
||||||
|
<span className={'value-warning'}>{value.warning}</span>
|
||||||
|
<span className={'value-error'}>{value.error}</span>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row gutter={[24, 20]} className={styles.row}>
|
||||||
|
{overviewConfigs.map((config, index) => (
|
||||||
|
<Col
|
||||||
|
xs={{ flex: '100%' }}
|
||||||
|
sm={{ flex: '50%' }}
|
||||||
|
md={{ flex: '50%' }}
|
||||||
|
lg={{ flex: '25%' }}
|
||||||
|
xl={{ flex: '25%' }}
|
||||||
|
key={config.key}
|
||||||
|
>
|
||||||
|
{renderCardItem({
|
||||||
|
label: intl.formatMessage({ id: config.label }),
|
||||||
|
value: renderValue(_.get(data, config.key, 0)),
|
||||||
|
bgColor: config.backgroundColor
|
||||||
|
})}
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Overview;
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import LineChart from '@/components/echarts/line-chart';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { memo, useContext, useMemo } from 'react';
|
||||||
|
import { DashboardContext } from '../config/dashboard-context';
|
||||||
|
|
||||||
|
const chartColorMap = {
|
||||||
|
tickLineColor: 'rgba(217,217,217,0.5)',
|
||||||
|
axislabelColor: 'rgba(0, 0, 0, 0.4)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const TypeKeyMap = {
|
||||||
|
cpu: {
|
||||||
|
label: 'CPU',
|
||||||
|
type: 'CPU',
|
||||||
|
intl: false,
|
||||||
|
color: 'rgba(250, 173, 20,.8)'
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
label: 'dashboard.memory',
|
||||||
|
type: 'Memory',
|
||||||
|
intl: true,
|
||||||
|
color: 'rgba(114, 46, 209,.8)'
|
||||||
|
},
|
||||||
|
gpu: {
|
||||||
|
label: 'GPU',
|
||||||
|
type: 'GPU',
|
||||||
|
intl: false,
|
||||||
|
color: 'rgba(84, 204, 152,.8)'
|
||||||
|
},
|
||||||
|
gpu_memory: {
|
||||||
|
label: 'dashboard.vram',
|
||||||
|
type: 'VRAM',
|
||||||
|
intl: true,
|
||||||
|
color: 'rgba(255, 107, 179, 80%)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: ''
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
itemWidth: 8,
|
||||||
|
itemHeight: 8,
|
||||||
|
data: []
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 0,
|
||||||
|
right: 20,
|
||||||
|
bottom: 20,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter(params: any) {
|
||||||
|
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
|
||||||
|
params.forEach((item: any) => {
|
||||||
|
result += `<span class="tooltip-item">
|
||||||
|
<span class="tooltip-item-name">
|
||||||
|
<span style="display:inline-block;margin-right:5px;border-radius:8px;width:8px;height:8px;background-color:${item.color};"></span>
|
||||||
|
<span class="tooltip-title">${item.seriesName}</span>:
|
||||||
|
</span>
|
||||||
|
<span class="tooltip-value">${item.data.value}</span>
|
||||||
|
</span>`;
|
||||||
|
});
|
||||||
|
return `<div class="tooltip-wrapper">${result}</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: true,
|
||||||
|
axisTick: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: chartColorMap.tickLineColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: chartColorMap.axislabelColor,
|
||||||
|
// fontFamily: 'unset',
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data: []
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
max: 100,
|
||||||
|
min: 0,
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
type: 'dashed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: chartColorMap.axislabelColor,
|
||||||
|
// fontFamily: 'unset',
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const UtilizationOvertime: React.FC = () => {
|
||||||
|
console.log('systemload=====================');
|
||||||
|
const intl = useIntl();
|
||||||
|
const data = useContext(DashboardContext)?.system_load?.history || {};
|
||||||
|
|
||||||
|
const typeList = ['gpu', 'cpu', 'memory', 'gpu_memory'];
|
||||||
|
|
||||||
|
const tooltipValueFormatter = (value: any) => {
|
||||||
|
return !value ? value : `${value}%`;
|
||||||
|
};
|
||||||
|
const generateData = useMemo(() => {
|
||||||
|
const legendData: string[] = [];
|
||||||
|
const xAxisData: string[] = [];
|
||||||
|
let seriesData: { value: number; time: string; type: string }[] = [];
|
||||||
|
seriesData = _.map(typeList, (item: string) => {
|
||||||
|
const itemConfig = _.get(TypeKeyMap, item, {});
|
||||||
|
const name = itemConfig.intl
|
||||||
|
? intl.formatMessage({ id: itemConfig.label })
|
||||||
|
: itemConfig.label;
|
||||||
|
legendData.push(name);
|
||||||
|
const itemDataList = _.get(data, item, []);
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
color: itemConfig.color,
|
||||||
|
data: _.map(itemDataList, (item: any) => {
|
||||||
|
xAxisData.push(dayjs(item.timestamp * 1000).format('HH:mm:ss'));
|
||||||
|
return {
|
||||||
|
time: dayjs(item.timestamp * 1000).format('HH:mm:ss'),
|
||||||
|
value: _.round(_.get(item, 'value', 0), 1)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
seriesData,
|
||||||
|
legendData,
|
||||||
|
xAxisData: _.uniq(xAxisData)
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LineChart
|
||||||
|
height={390}
|
||||||
|
seriesData={generateData.seriesData}
|
||||||
|
legendData={generateData.legendData}
|
||||||
|
xAxisData={generateData.xAxisData}
|
||||||
|
tooltipValueFormatter={tooltipValueFormatter}
|
||||||
|
smooth={true}
|
||||||
|
width="100%"
|
||||||
|
yAxisName="(%)"
|
||||||
|
></LineChart>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(UtilizationOvertime);
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import CardWrapper from '@/components/card-wrapper';
|
||||||
|
import GaugeChart from '@/components/echarts/gauge';
|
||||||
|
import PageTools from '@/components/page-tools';
|
||||||
|
import breakpoints from '@/config/breakpoints';
|
||||||
|
import useWindowResize from '@/hooks/use-window-resize';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Col, Row } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
import { DashboardContext } from '../config/dashboard-context';
|
||||||
|
import ResourceUtilization from './resource-utilization';
|
||||||
|
|
||||||
|
const strokeColorFunc = (percent: number) => {
|
||||||
|
if (percent <= 50) {
|
||||||
|
return 'rgb(84, 204, 152, 80%)';
|
||||||
|
}
|
||||||
|
if (percent <= 80) {
|
||||||
|
return 'rgba(250, 173, 20, 80%)';
|
||||||
|
}
|
||||||
|
return ' rgba(255, 77, 79, 80%)';
|
||||||
|
};
|
||||||
|
|
||||||
|
const SystemLoad = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const data = useContext(DashboardContext)?.system_load?.current || {};
|
||||||
|
const { size } = useWindowResize();
|
||||||
|
const [paddingRight, setPaddingRight] = useState<string>('20px');
|
||||||
|
const [smallChartHeight, setSmallChartHeight] = useState<number>(190);
|
||||||
|
const [largeChartHeight, setLargeChartHeight] = useState<number>(400);
|
||||||
|
const thresholds = [0.5, 0.7, 1];
|
||||||
|
const height = 400;
|
||||||
|
const currentDate = dayjs().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const handleSelectDate = (date: any) => {};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (size.width < breakpoints.xl) {
|
||||||
|
setPaddingRight('0');
|
||||||
|
} else {
|
||||||
|
setPaddingRight('20px');
|
||||||
|
}
|
||||||
|
}, [size.width]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="system-load">
|
||||||
|
<PageTools
|
||||||
|
style={{ margin: '26px 0px' }}
|
||||||
|
left={
|
||||||
|
<span>{intl.formatMessage({ id: 'dashboard.systemload' })}</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Row style={{ width: '100%' }} gutter={[0, 20]}>
|
||||||
|
<Col
|
||||||
|
xs={24}
|
||||||
|
sm={24}
|
||||||
|
md={24}
|
||||||
|
lg={24}
|
||||||
|
xl={16}
|
||||||
|
style={{ paddingRight: paddingRight }}
|
||||||
|
>
|
||||||
|
<CardWrapper style={{ height: height, width: '100%' }}>
|
||||||
|
<ResourceUtilization />
|
||||||
|
</CardWrapper>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={24} lg={24} xl={8}>
|
||||||
|
<CardWrapper style={{ height: largeChartHeight, width: '100%' }}>
|
||||||
|
<Row style={{ height: largeChartHeight, width: '100%' }}>
|
||||||
|
<Col span={12} style={{ height: smallChartHeight }}>
|
||||||
|
<GaugeChart
|
||||||
|
height={smallChartHeight}
|
||||||
|
value={_.round(data.gpu || 0, 1)}
|
||||||
|
color={strokeColorFunc(data.gpu)}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'dashboard.gpuutilization'
|
||||||
|
})}
|
||||||
|
></GaugeChart>
|
||||||
|
</Col>
|
||||||
|
<Col span={12} style={{ height: smallChartHeight }}>
|
||||||
|
<GaugeChart
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'dashboard.vramutilization'
|
||||||
|
})}
|
||||||
|
height={smallChartHeight}
|
||||||
|
color={strokeColorFunc(data.gpu_memory)}
|
||||||
|
value={_.round(data.gpu_memory || 0, 1)}
|
||||||
|
></GaugeChart>
|
||||||
|
</Col>
|
||||||
|
<Col span={12} style={{ height: smallChartHeight }}>
|
||||||
|
<GaugeChart
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'dashboard.cpuutilization'
|
||||||
|
})}
|
||||||
|
height={smallChartHeight}
|
||||||
|
color={strokeColorFunc(data.cpu)}
|
||||||
|
value={_.round(data.cpu || 0, 1)}
|
||||||
|
></GaugeChart>
|
||||||
|
</Col>
|
||||||
|
<Col span={12} style={{ height: smallChartHeight }}>
|
||||||
|
<GaugeChart
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'dashboard.memoryutilization'
|
||||||
|
})}
|
||||||
|
height={smallChartHeight}
|
||||||
|
color={strokeColorFunc(data.memory)}
|
||||||
|
value={_.round(data.memory || 0, 1)}
|
||||||
|
></GaugeChart>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</CardWrapper>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemLoad;
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import PageTools from '@/components/page-tools';
|
||||||
|
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { memo, useCallback, useContext } from 'react';
|
||||||
|
import { DashboardContext } from '../../config/dashboard-context';
|
||||||
|
import RequestTokenInner from './request-token-inner';
|
||||||
|
|
||||||
|
const baseColorMap = {
|
||||||
|
baseL2: 'rgba(13,171,219,0.8)',
|
||||||
|
baseL1: 'rgba(0,34,255,0.8)',
|
||||||
|
base: 'rgba(0,85,255,0.8)',
|
||||||
|
baseR1: 'rgba(0,255,233,0.8)',
|
||||||
|
baseR2: 'rgba(48,0,255,0.8)',
|
||||||
|
baseR3: 'rgba(85,167,255,0.8)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentMonthDays = () => {
|
||||||
|
const now = dayjs();
|
||||||
|
const daysInMonth = now.daysInMonth();
|
||||||
|
const year = dayjs().year();
|
||||||
|
const month = dayjs().month() + 1;
|
||||||
|
|
||||||
|
const dates = [];
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
dates.push(dayjs(`${year}-${month}-${day}`).format('YYYY-MM-DD'));
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const currentDate = dayjs();
|
||||||
|
const currentMonthDays = getCurrentMonthDays();
|
||||||
|
let requestData: {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
areaStyle: any;
|
||||||
|
data: { time: string; value: number }[];
|
||||||
|
}[] = [];
|
||||||
|
let tokenData: { time: string; value: number }[] = [];
|
||||||
|
let userData: { name: string; value: number }[] = [];
|
||||||
|
const xAxisData: string[] = currentMonthDays;
|
||||||
|
let topUserList: string[] = [];
|
||||||
|
|
||||||
|
const { model_usage } = useContext(DashboardContext);
|
||||||
|
const data = model_usage || {};
|
||||||
|
|
||||||
|
const handleSelectDate = (date: any) => {
|
||||||
|
// fetchData?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateData = useCallback(() => {
|
||||||
|
const requestList: {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
areaStyle: any;
|
||||||
|
data: { time: string; value: number }[];
|
||||||
|
} = {
|
||||||
|
name: 'API requests',
|
||||||
|
areaStyle: {},
|
||||||
|
color: baseColorMap.base,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const completionData: any = {
|
||||||
|
name: 'Completion tokens',
|
||||||
|
color: baseColorMap.base,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
const prompData: any = {
|
||||||
|
name: 'Prompt tokens',
|
||||||
|
color: baseColorMap.baseR3,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const topUserPrompt: any = {
|
||||||
|
name: 'Prompt tokens',
|
||||||
|
color: baseColorMap.baseR3,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
const topUserCompletion: any = {
|
||||||
|
name: 'Completion tokens',
|
||||||
|
color: baseColorMap.base,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
_.each(xAxisData, (date: string) => {
|
||||||
|
// tokens data
|
||||||
|
const item = _.find(data.completion_token_history, (item: any) => {
|
||||||
|
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
||||||
|
});
|
||||||
|
if (!item) {
|
||||||
|
completionData.data.push({
|
||||||
|
titme: date,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
completionData.data.push({
|
||||||
|
value: item.value,
|
||||||
|
time: dayjs(item.timestamp * 1000).format('YYYY-MM-DD')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptItem = _.find(data.prompt_token_history, (item: any) => {
|
||||||
|
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
||||||
|
});
|
||||||
|
if (!promptItem) {
|
||||||
|
prompData.data.push({
|
||||||
|
value: 0,
|
||||||
|
time: date
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
prompData.data.push({
|
||||||
|
value: promptItem.value,
|
||||||
|
time: dayjs(promptItem.timestamp * 1000).format('YYYY-MM-DD')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== api request data =================
|
||||||
|
const requestItem = _.find(data.api_request_history, (item: any) => {
|
||||||
|
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!requestItem) {
|
||||||
|
requestList.data.push({
|
||||||
|
time: date,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
requestList.data.push({
|
||||||
|
time: dayjs(requestItem.timestamp * 1000).format('YYYY-MM-DD'),
|
||||||
|
value: requestItem.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== top users ============
|
||||||
|
if (!data.top_users?.length) {
|
||||||
|
userData = [];
|
||||||
|
topUserList = [];
|
||||||
|
} else {
|
||||||
|
const users: string[] = [];
|
||||||
|
_.each(data.top_users, (item: any) => {
|
||||||
|
users.push(item.username);
|
||||||
|
topUserPrompt.data.push({
|
||||||
|
name: item.username,
|
||||||
|
value: item.prompt_token_count
|
||||||
|
});
|
||||||
|
topUserCompletion.data.push({
|
||||||
|
name: item.username,
|
||||||
|
value: item.completion_token_count
|
||||||
|
});
|
||||||
|
});
|
||||||
|
topUserList = _.uniq(users);
|
||||||
|
userData = [topUserCompletion, topUserPrompt];
|
||||||
|
}
|
||||||
|
|
||||||
|
requestData = [requestList];
|
||||||
|
tokenData = [completionData, prompData];
|
||||||
|
}, [data, xAxisData]);
|
||||||
|
|
||||||
|
generateData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTools />
|
||||||
|
<RequestTokenInner
|
||||||
|
requestData={requestData}
|
||||||
|
xAxisData={xAxisData}
|
||||||
|
tokenData={tokenData}
|
||||||
|
paddingRight={paddingRight}
|
||||||
|
></RequestTokenInner>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(UsageInner);
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import CardWrapper from '@/components/card-wrapper';
|
||||||
|
import BarChart from '@/components/echarts/bar-chart';
|
||||||
|
import LineChart from '@/components/echarts/line-chart';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Col, Row } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface RequestTokenInnerProps {
|
||||||
|
requestData: {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
areaStyle: any;
|
||||||
|
data: { time: string; value: number }[];
|
||||||
|
}[];
|
||||||
|
tokenData: { time: string; value: number }[];
|
||||||
|
xAxisData: string[];
|
||||||
|
}
|
||||||
|
const RequestTokenInner: React.FC<
|
||||||
|
RequestTokenInnerProps & { paddingRight: string }
|
||||||
|
> = (props) => {
|
||||||
|
console.log('request token inner=====================');
|
||||||
|
const { requestData, tokenData, xAxisData, paddingRight } = props;
|
||||||
|
const intl = useIntl();
|
||||||
|
const labelFormatter = (v: any) => {
|
||||||
|
return dayjs(v).format('MM-DD');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Row style={{ width: '100%' }} gutter={[0, 20]}>
|
||||||
|
<Col
|
||||||
|
xs={24}
|
||||||
|
sm={24}
|
||||||
|
md={24}
|
||||||
|
lg={24}
|
||||||
|
xl={12}
|
||||||
|
style={{ paddingRight: paddingRight }}
|
||||||
|
>
|
||||||
|
<CardWrapper style={{ width: '100%' }}>
|
||||||
|
<LineChart
|
||||||
|
title={intl.formatMessage({ id: 'dashboard.apirequest' })}
|
||||||
|
seriesData={requestData}
|
||||||
|
xAxisData={xAxisData}
|
||||||
|
height={360}
|
||||||
|
labelFormatter={labelFormatter}
|
||||||
|
></LineChart>
|
||||||
|
</CardWrapper>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={24} lg={24} xl={12}>
|
||||||
|
<CardWrapper style={{ width: '100%' }}>
|
||||||
|
<BarChart
|
||||||
|
title={intl.formatMessage({ id: 'dashboard.tokens' })}
|
||||||
|
seriesData={tokenData}
|
||||||
|
xAxisData={xAxisData}
|
||||||
|
height={360}
|
||||||
|
labelFormatter={labelFormatter}
|
||||||
|
></BarChart>
|
||||||
|
</CardWrapper>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(RequestTokenInner);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import CardWrapper from '@/components/card-wrapper';
|
||||||
|
import HBar from '@/components/echarts/h-bar';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TopUserProps {
|
||||||
|
userData: { name: string; value: number }[];
|
||||||
|
topUserList: string[];
|
||||||
|
}
|
||||||
|
const TopUser: React.FC<TopUserProps> = (props) => {
|
||||||
|
console.log('TopUser=====================');
|
||||||
|
const { userData, topUserList } = props;
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardWrapper>
|
||||||
|
<HBar
|
||||||
|
title={intl.formatMessage({ id: 'dashboard.topusers' })}
|
||||||
|
seriesData={userData}
|
||||||
|
xAxisData={topUserList}
|
||||||
|
height={360}
|
||||||
|
></HBar>
|
||||||
|
</CardWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TopUser);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import breakpoints from '@/config/breakpoints';
|
||||||
|
import useWindowResize from '@/hooks/use-window-resize';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import UserInner from './usage-inner';
|
||||||
|
|
||||||
|
const Usage = () => {
|
||||||
|
const {
|
||||||
|
size: { width }
|
||||||
|
} = useWindowResize();
|
||||||
|
const [paddingRight, setPaddingRight] = useState<string>('20px');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (width < breakpoints.xl) {
|
||||||
|
setPaddingRight('0');
|
||||||
|
} else {
|
||||||
|
setPaddingRight('20px');
|
||||||
|
}
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
return <UserInner paddingRight={paddingRight}></UserInner>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Usage;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import { DashboardProps } from './types';
|
||||||
|
|
||||||
|
export const DashboardContext = createContext<
|
||||||
|
DashboardProps & { fetchData: () => Promise<void> }
|
||||||
|
>({} as DashboardProps & { fetchData: () => Promise<void> });
|
||||||
|
|
||||||
|
export default DashboardContext;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export const overviewConfigs = [
|
||||||
|
{
|
||||||
|
key: 'worker_count',
|
||||||
|
label: 'dashboard.workers',
|
||||||
|
backgroundColor: 'var(--color-white-1)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gpu_count',
|
||||||
|
label: 'dashboard.totalgpus',
|
||||||
|
backgroundColor: 'var(--color-white-1)'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: 'model_count',
|
||||||
|
label: 'dashboard.models',
|
||||||
|
backgroundColor: 'var(--color-white-1)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'model_instance_count',
|
||||||
|
label: 'models.form.replicas',
|
||||||
|
backgroundColor: 'var(--color-white-1)'
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export interface DashboardProps {
|
||||||
|
resource_counts: {
|
||||||
|
worker_count: number;
|
||||||
|
gpu_count: number;
|
||||||
|
model_count: number;
|
||||||
|
model_instance_count: number;
|
||||||
|
};
|
||||||
|
system_load: {
|
||||||
|
current: {
|
||||||
|
cpu: number;
|
||||||
|
memory: number;
|
||||||
|
gpu: number;
|
||||||
|
gpu_memory: number;
|
||||||
|
};
|
||||||
|
history: {
|
||||||
|
cpu: {
|
||||||
|
timestamp: number;
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
memory: {
|
||||||
|
timestamp: number;
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
gpu: {
|
||||||
|
timestamp: number;
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
gpu_memory: {
|
||||||
|
timestamp: number;
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
model_usage: {
|
||||||
|
api_request_history: any[];
|
||||||
|
completion_token_history: any[];
|
||||||
|
prompt_token_history: any[];
|
||||||
|
top_users: {
|
||||||
|
user_id: number;
|
||||||
|
username: string;
|
||||||
|
prompt_token_count: number;
|
||||||
|
completion_token_count: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
active_models: any[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import DashboardInner from './components/dahboard-inner';
|
||||||
|
|
||||||
|
const Dashboard: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageContainer ghost extra={[]}>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<DashboardInner setLoading={setLoading} />
|
||||||
|
</Spin>
|
||||||
|
</PageContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Dashboard);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.value-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.value-healthy {
|
||||||
|
color: var(--ant-green-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-warning {
|
||||||
|
color: var(--ant-gold-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-error {
|
||||||
|
color: var(--ant-red-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,4 +21,3 @@
|
|||||||
},
|
},
|
||||||
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"]
|
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user