Browse Source

feat: 完成模型指标对比

pull/137/head
cp3hnu 1 year ago
parent
commit
4c24faafe9
23 changed files with 897 additions and 162 deletions
  1. BIN
      react-ui/src/assets/img/metrics-title-icon.png
  2. BIN
      react-ui/src/assets/img/model-metrics.png
  3. +43
    -37
      react-ui/src/components/BasicInfo/index.less
  4. +70
    -36
      react-ui/src/components/BasicInfo/index.tsx
  5. +64
    -0
      react-ui/src/components/BasicTableInfo/index.less
  6. +43
    -0
      react-ui/src/components/BasicTableInfo/index.tsx
  7. +6
    -4
      react-ui/src/pages/Dataset/components/ResourceInfo/index.less
  8. +10
    -1
      react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx
  9. +21
    -6
      react-ui/src/pages/Dataset/components/ResourceIntro/index.less
  10. +67
    -67
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  11. +5
    -0
      react-ui/src/pages/Dataset/components/ResourceVersion/index.less
  12. +1
    -1
      react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx
  13. +4
    -2
      react-ui/src/pages/Experiment/Comparison/index.tsx
  14. +5
    -3
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  15. +2
    -1
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  16. +29
    -0
      react-ui/src/pages/Model/components/MetricsChart/index.less
  17. +174
    -0
      react-ui/src/pages/Model/components/MetricsChart/index.tsx
  18. +33
    -0
      react-ui/src/pages/Model/components/MetricsChart/tooltip.css
  19. +5
    -2
      react-ui/src/pages/Model/components/ModelEvolution/index.less
  20. +35
    -0
      react-ui/src/pages/Model/components/ModelMetrics/index.less
  21. +259
    -0
      react-ui/src/pages/Model/components/ModelMetrics/index.tsx
  22. +16
    -0
      react-ui/src/services/dataset/index.js
  23. +5
    -2
      react-ui/src/services/experiment/index.js

BIN
react-ui/src/assets/img/metrics-title-icon.png View File

Before After
Width: 39  |  Height: 39  |  Size: 536 B

BIN
react-ui/src/assets/img/model-metrics.png View File

Before After
Width: 45  |  Height: 45  |  Size: 1.2 kB

+ 43
- 37
react-ui/src/components/BasicInfo/index.less View File

@@ -5,48 +5,54 @@
gap: 20px 40px; gap: 20px 40px;
align-items: flex-start; align-items: flex-start;
width: 80%; width: 80%;
}


.kf-basic-info-item {
display: flex;
align-items: flex-start;
width: calc(50% - 20px);
font-size: 16px;
line-height: 1.6;

&__label {
position: relative;
flex: none;
color: @text-color-secondary;
text-align: justify;
text-align-last: justify;

&::after {
position: absolute;
content: ':';
&__item {
display: flex;
align-items: flex-start;
width: calc(50% - 20px);

&__label {
position: relative;
flex: none;
color: @text-color-secondary;
font-size: @font-size-content;
line-height: 1.6;
text-align: justify;
text-align-last: justify;

&::after {
position: absolute;
content: ':';
}
} }
}


&__list-value {
display: flex;
flex: 1;
flex-direction: column;
gap: 5px 0;
}
&__value-container {
display: flex;
flex: 1;
flex-direction: column;
gap: 5px 0;
}


&__value {
flex: 1;
margin-left: 16px;
white-space: pre-line;
word-break: break-all;
}
&__value {
flex: 1;
margin-left: 16px;
font-size: @font-size-content;
line-height: 1.6;
white-space: pre-line;
word-break: break-all;


&__text {
color: @text-color;
}
&--ellipsis {
.singleLine();
}

&__text {
color: @text-color;
}


&__link:hover {
text-decoration: underline @underline-color;
text-underline-offset: 3px;
&__link:hover {
text-decoration: underline @underline-color;
text-underline-offset: 3px;
}
}
} }
} }

+ 70
- 36
react-ui/src/components/BasicInfo/index.tsx View File

@@ -1,4 +1,5 @@
import { Link } from '@umijs/max'; import { Link } from '@umijs/max';
import { Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import './index.less'; import './index.less';


@@ -11,6 +12,7 @@ export type BasicInfoLink = {
export type BasicInfoData = { export type BasicInfoData = {
label: string; label: string;
value?: any; value?: any;
ellipsis?: boolean;
format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined;
}; };


@@ -18,45 +20,73 @@ type BasicInfoProps = {
datas: BasicInfoData[]; datas: BasicInfoData[];
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
labelWidth?: number;
labelWidth: number;
}; };


function BasicInfo({ datas, className, style, labelWidth = 100 }: BasicInfoProps) {
type BasicInfoItemProps = {
data: BasicInfoData;
labelWidth: number;
classPrefix: string;
};

type BasicInfoItemValueProps = BasicInfoLink & {
ellipsis?: boolean;
classPrefix: string;
};

export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) {
return ( return (
<div className={classNames('kf-basic-info', className)} style={style}> <div className={classNames('kf-basic-info', className)} style={style}>
{datas.map((item) => ( {datas.map((item) => (
<BasicInfoItem key={item.label} data={item} labelWidth={labelWidth} />
<BasicInfoItem
key={item.label}
data={item}
labelWidth={labelWidth}
classPrefix="kf-basic-info"
/>
))} ))}
</div> </div>
); );
} }


type BasicInfoItemProps = {
data: BasicInfoData;
labelWidth?: number;
};
function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) {
const { label, value, format } = data;
export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) {
const { label, value, format, ellipsis } = data;
const formatValue = format ? format(value) : value; const formatValue = format ? format(value) : value;
const myClassName = `${classPrefix}__item`;
let valueComponent = undefined; let valueComponent = undefined;
if (Array.isArray(formatValue)) { if (Array.isArray(formatValue)) {
valueComponent = ( valueComponent = (
<div className="kf-basic-info-item__list-value">
<div className={`${myClassName}__value-container`}>
{formatValue.map((item: BasicInfoLink) => ( {formatValue.map((item: BasicInfoLink) => (
<BasicInfoItemValue key={item.value} value={item.value} link={item.link} url={item.url} />
<BasicInfoItemValue
key={item.value}
value={item.value}
link={item.link}
url={item.url}
ellipsis={ellipsis}
classPrefix={classPrefix}
/>
))} ))}
</div> </div>
); );
} else if (typeof formatValue === 'object' && formatValue) { } else if (typeof formatValue === 'object' && formatValue) {
valueComponent = ( valueComponent = (
<BasicInfoItemValue value={formatValue.value} link={formatValue.link} url={formatValue.url} />
<BasicInfoItemValue
value={formatValue.value}
link={formatValue.link}
url={formatValue.url}
ellipsis={ellipsis}
classPrefix={classPrefix}
/>
); );
} else { } else {
valueComponent = <BasicInfoItemValue value={formatValue} />;
valueComponent = (
<BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} />
);
} }
return ( return (
<div className="kf-basic-info-item" key={label}>
<div className="kf-basic-info-item__label" style={{ width: labelWidth }}>
<div className={myClassName} key={label}>
<div className={`${myClassName}__label`} style={{ width: labelWidth }}>
{label} {label}
</div> </div>
{valueComponent} {valueComponent}
@@ -64,35 +94,39 @@ function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) {
); );
} }


type BasicInfoItemValueProps = {
value: string;
link?: string;
url?: string;
};

function BasicInfoItemValue({ value, link, url }: BasicInfoItemValueProps) {
export function BasicInfoItemValue({
value,
link,
url,
ellipsis,
classPrefix,
}: BasicInfoItemValueProps) {
const myClassName = `${classPrefix}__item__value`;
let component = undefined;
if (url && value) { if (url && value) {
return (
<a
className="kf-basic-info-item__value kf-basic-info-item__link"
href={url}
target="_blank"
rel="noopener noreferrer"
>
component = (
<a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer">
{value} {value}
</a> </a>
); );
} else if (link && value) { } else if (link && value) {
return (
<Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link">
component = (
<Link to={link} className={`${myClassName}__link`}>
{value} {value}
</Link> </Link>
); );
} else { } else {
return (
<div className="kf-basic-info-item__value kf-basic-info-item__text">{value ?? '--'}</div>
);
component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>;
} }
}


export default BasicInfo;
return (
<Typography.Text
className={classNames(myClassName, {
[`${myClassName}--ellipsis`]: ellipsis,
})}
ellipsis={{ tooltip: value }}
>
{component}
</Typography.Text>
);
}

+ 64
- 0
react-ui/src/components/BasicTableInfo/index.less View File

@@ -0,0 +1,64 @@
.kf-basic-table-info {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
border: 1px solid @border-color-base;
border-bottom: none;
border-radius: 4px;

&__item {
display: flex;
align-items: stretch;
width: 25%;
border-bottom: 1px solid @border-color-base;

&__label {
flex: none;
padding: 12px 20px;
color: @text-color-secondary;
font-size: 14px;
text-align: left;
background-color: .addAlpha(#606b7a, 0.05) [];
}

&__value-container {
display: flex;
flex: 1;
flex-direction: column;
align-items: flex-start;
min-width: 0;
}

&__value {
flex: 1;
margin: 0 !important;
padding: 12px 20px 4px;
font-size: @font-size;
white-space: pre-line;
word-break: break-all;

& + & {
padding-top: 0;
}

&:last-child {
padding-bottom: 12px;
}

&--ellipsis {
.singleLine();
}

&__text {
color: @text-color;
}

&__link:hover {
text-decoration: underline @underline-color;
text-underline-offset: 3px;
}
}
}
}

+ 43
- 0
react-ui/src/components/BasicTableInfo/index.tsx View File

@@ -0,0 +1,43 @@
import classNames from 'classnames';
import { BasicInfoItem, type BasicInfoData, type BasicInfoLink } from '../BasicInfo';
import './index.less';
export type { BasicInfoData, BasicInfoLink };

type BasicTableInfoProps = {
datas: BasicInfoData[];
className?: string;
style?: React.CSSProperties;
labelWidth: number;
};

export default function BasicTableInfo({
datas,
className,
style,
labelWidth,
}: BasicTableInfoProps) {
const remainder = datas.length % 4;
const array = [];
if (remainder > 0) {
for (let i = 0; i < 4 - remainder; i++) {
array.push({
label: '',
value: '',
});
}
}
const showDatas = [...datas, ...array];

return (
<div className={classNames('kf-basic-table-info', className)} style={style}>
{showDatas.map((item) => (
<BasicInfoItem
key={item.label}
data={item}
labelWidth={labelWidth}
classPrefix="kf-basic-table-info"
/>
))}
</div>
);
}

+ 6
- 4
react-ui/src/pages/Dataset/components/ResourceInfo/index.less View File

@@ -38,10 +38,6 @@
&__bottom { &__bottom {
position: relative; position: relative;
height: calc(100% - 135px); height: calc(100% - 135px);
padding: 8px 30px 20px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);


&__legend { &__legend {
position: absolute; position: absolute;
@@ -52,6 +48,12 @@
:global { :global {
.ant-tabs { .ant-tabs {
height: 100%; height: 100%;
.ant-tabs-nav-wrap {
padding-top: 8px;
padding-left: 30px;
background-color: white;
border-radius: 10px 10px 0 0;
}
.ant-tabs-content-holder { .ant-tabs-content-holder {
height: 100%; height: 100%;
.ant-tabs-content { .ant-tabs-content {


+ 10
- 1
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -164,7 +164,16 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
key: ResourceInfoTabKeys.Introduction, key: ResourceInfoTabKeys.Introduction,
label: `${typeName}简介`, label: `${typeName}简介`,
icon: <KFIcon type="icon-moxingjianjie" />, icon: <KFIcon type="icon-moxingjianjie" />,
children: <ResourceIntro resourceType={resourceType} info={info}></ResourceIntro>,
children: (
<ResourceIntro
resourceType={resourceType}
info={info}
resourceId={resourceId}
identifier={identifier}
owner={owner}
version={version}
></ResourceIntro>
),
}, },
{ {
key: ResourceInfoTabKeys.Version, key: ResourceInfoTabKeys.Version,


+ 21
- 6
react-ui/src/pages/Dataset/components/ResourceIntro/index.less View File

@@ -1,10 +1,25 @@
.resource-intro { .resource-intro {
width: 100%; width: 100%;
margin-top: 24px;
&__basic {
width: 100%;
}
&__usage {
width: 100%;

&__top {
padding: 20px 30px;
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);

pre {
margin-bottom: 0 !important;
}

&__title {
margin: 15px 0;
color: @text-color-secondary;
font-size: 14px;
}

&__desc {
color: @text-color;
font-size: @font-size;
}
} }
} }

+ 67
- 67
react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx View File

@@ -1,4 +1,4 @@
import BasicInfo, { BasicInfoData } from '@/components/BasicInfo';
import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo';
import { import {
@@ -8,13 +8,19 @@ import {
ProjectDependency, ProjectDependency,
ResourceType, ResourceType,
TrainTask, TrainTask,
resourceConfig,
} from '@/pages/Dataset/config'; } from '@/pages/Dataset/config';
import ModelMetrics from '@/pages/Model/components/ModelMetrics';
import { getGitUrl } from '@/utils'; import { getGitUrl } from '@/utils';
import styles from './index.less'; import styles from './index.less';


type ResourceIntroProps = { type ResourceIntroProps = {
resourceType: ResourceType; resourceType: ResourceType;
info: DatasetData | ModelData; info: DatasetData | ModelData;
resourceId: number;
identifier: string;
owner: string;
version?: string;
}; };


const formatDataset = (datasets?: DatasetData[]) => { const formatDataset = (datasets?: DatasetData[]) => {
@@ -27,29 +33,6 @@ const formatDataset = (datasets?: DatasetData[]) => {
})); }));
}; };


const formatParams = (map?: Record<string, string>, space: string = '') => {
if (!map || Object.keys(map).length === 0) {
return undefined;
}
return Object.entries(map)
.map(([key, value]) => `${space}${key} : ${value}`)
.join('\n');
};

const formatMetrics = (map?: Record<string, string>) => {
if (!map || Object.keys(map).length === 0) {
return undefined;
}
return Object.entries(map)
.map(([key, value]) => {
if (typeof value === 'object' && value !== null) {
return `${key} : \n${formatParams(value, ' ')}`;
}
return `${key} : ${value}`;
})
.join('\n');
};

const getProjectUrl = (project?: ProjectDependency) => { const getProjectUrl = (project?: ProjectDependency) => {
if (!project || !project.url || !project.branch) { if (!project || !project.url || !project.branch) {
return undefined; return undefined;
@@ -93,49 +76,50 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [
{ {
label: '数据集名称', label: '数据集名称',
value: data.name, value: data.name,
ellipsis: true,
}, },
{ {
label: '版本', label: '版本',
value: data.version, value: data.version,
ellipsis: true,
}, },
{ {
label: '创建人', label: '创建人',
value: data.create_by, value: data.create_by,
ellipsis: true,
}, },
{ {
label: '更新时间', label: '更新时间',
value: data.update_time, value: data.update_time,
ellipsis: true,
}, },
{ {
label: '数据来源', label: '数据来源',
value: data.dataset_source, value: data.dataset_source,
format: formatSource, format: formatSource,
ellipsis: true,
}, },
{ {
label: '训练任务', label: '训练任务',
value: data.train_task, value: data.train_task,
format: formatTrainTask, format: formatTrainTask,
ellipsis: true,
}, },
{ {
label: '处理代码', label: '处理代码',
value: data.processing_code, value: data.processing_code,
format: formatProject, format: formatProject,
ellipsis: true,
}, },
{ {
label: '数据集分类', label: '数据集分类',
value: data.data_type, value: data.data_type,
ellipsis: true,
}, },
{ {
label: '研究方向', label: '研究方向',
value: data.data_tag, value: data.data_tag,
},
{
label: '数据集描述',
value: data.description,
},
{
label: '版本描述',
value: data.version_desc,
ellipsis: true,
}, },
]; ];


@@ -143,77 +127,79 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [
{ {
label: '模型名称', label: '模型名称',
value: data.name, value: data.name,
ellipsis: true,
}, },
{ {
label: '版本', label: '版本',
value: data.version, value: data.version,
ellipsis: true,
}, },
{ {
label: '创建人', label: '创建人',
value: data.create_by, value: data.create_by,
ellipsis: true,
}, },
{ {
label: '更新时间', label: '更新时间',
value: data.update_time, value: data.update_time,
ellipsis: true,
}, },
{ {
label: '训练镜像', label: '训练镜像',
value: data.image, value: data.image,
ellipsis: true,
}, },
{ {
label: '训练代码', label: '训练代码',
value: data.project_depency, value: data.project_depency,
format: formatProject, format: formatProject,
ellipsis: true,
}, },
{ {
label: '训练数据集', label: '训练数据集',
value: data.train_datasets, value: data.train_datasets,
format: formatDataset, format: formatDataset,
ellipsis: true,
}, },
{ {
label: '测试数据集', label: '测试数据集',
value: data.test_datasets, value: data.test_datasets,
format: formatDataset, format: formatDataset,
},
{
label: '参数',
value: data.params,
format: formatParams,
},
{
label: '指标',
value: data.metrics,
format: formatMetrics,
ellipsis: true,
}, },
{ {
label: '模型来源', label: '模型来源',
value: data.model_source, value: data.model_source,
format: formatSource, format: formatSource,
ellipsis: true,
}, },
{ {
label: '训练任务', label: '训练任务',
value: data.train_task, value: data.train_task,
format: formatTrainTask, format: formatTrainTask,
ellipsis: true,
}, },
{ {
label: '模型框架', label: '模型框架',
value: data.model_type, value: data.model_type,
ellipsis: true,
}, },
{ {
label: '模型能力', label: '模型能力',
value: data.model_tag, value: data.model_tag,
},
{
label: '模型描述',
value: data.description,
},
{
label: '版本描述',
value: data.version_desc,
ellipsis: true,
}, },
]; ];


function ResourceIntro({ resourceType, info }: ResourceIntroProps) {
function ResourceIntro({
resourceType,
info,
resourceId,
identifier,
owner,
version,
}: ResourceIntroProps) {
const config = resourceConfig[resourceType];
const basicDatas: BasicInfoData[] = const basicDatas: BasicInfoData[] =
resourceType === ResourceType.Dataset resourceType === ResourceType.Dataset
? getDatasetDatas(info as DatasetData) ? getDatasetDatas(info as DatasetData)
@@ -221,23 +207,37 @@ function ResourceIntro({ resourceType, info }: ResourceIntroProps) {


return ( return (
<div className={styles['resource-intro']}> <div className={styles['resource-intro']}>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['resource-intro__basic']}>
<BasicInfo datas={basicDatas} labelWidth={86}></BasicInfo>
<div className={styles['resource-intro__top']}>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '15px' }}
></SubAreaTitle>
<div className={styles['resource-intro__top__basic']}>
<BasicTableInfo datas={basicDatas} labelWidth={135}></BasicTableInfo>
</div>
<div className={styles['resource-intro__top__title']}>{`${config.name}描述`}</div>
<div className={styles['resource-intro__top__desc']}>{info.description ?? '暂无描述'}</div>
<div className={styles['resource-intro__top__title']}>版本描述</div>
<div className={styles['resource-intro__top__desc']}>{info.version_desc ?? '暂无描述'}</div>
<SubAreaTitle
title="实例用法"
image={require('@/assets/img/usage-icon.png')}
style={{ margin: '25px 0 15px' }}
></SubAreaTitle>
<div
className={styles['resource-intro__top__usage']}
dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }}
></div>
</div> </div>
<SubAreaTitle
title="实例用法"
image={require('@/assets/img/usage-icon.png')}
style={{ margin: '40px 0 24px' }}
></SubAreaTitle>
<div
className={styles['resource-intro__usage']}
dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }}
></div>
{resourceType === ResourceType.Model && version && (
<ModelMetrics
resourceId={resourceId}
identifier={identifier}
owner={owner}
version={version}
></ModelMetrics>
)}
</div> </div>
); );
} }


+ 5
- 0
react-ui/src/pages/Dataset/components/ResourceVersion/index.less View File

@@ -1,4 +1,9 @@
.resource-version { .resource-version {
min-height: 100%;
padding: 20px 30px;
color: @text-color; color: @text-color;
font-size: @font-size-content; font-size: @font-size-content;
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
} }

+ 1
- 1
react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx View File

@@ -86,7 +86,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) {


return ( return (
<div className={styles['resource-version']}> <div className={styles['resource-version']}>
<Flex justify="space-between" align="center" style={{ margin: '30px 0' }}>
<Flex justify="space-between" align="center" style={{ marginBottom: '20px' }}>
<Flex align="center"> <Flex align="center">
<Button <Button
type="default" type="default"


+ 4
- 2
react-ui/src/pages/Experiment/Comparison/index.tsx View File

@@ -53,7 +53,7 @@ function ExperimentComparison() {
// setLoading(true); // setLoading(true);
const request = const request =
comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq;
const [res] = await to(request(experimentId));
const [res] = await to(request(experimentId, { offset: '', limit: 50 }));
// setLoading(false); // setLoading(false);
if (res && res.data) { if (res && res.data) {
// const { content = [], totalElements = 0 } = res.data; // const { content = [], totalElements = 0 } = res.data;
@@ -202,9 +202,11 @@ function ExperimentComparison() {
dataSource={tableData} dataSource={tableData}
columns={columns} columns={columns}
rowSelection={rowSelection} rowSelection={rowSelection}
scroll={{ y: 'calc(100% - 55px)', x: '100%' }}
scroll={{ y: 'calc(100% - 110px)', x: '100%' }}
pagination={false} pagination={false}
bordered={true} bordered={true}
virtual
// onScroll={handleTableScroll}
// loading={loading} // loading={loading}
// pagination={{ // pagination={{
// ...pagination, // ...pagination,


+ 5
- 3
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -52,7 +52,7 @@ function LogGroup({
const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false);
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
const socketRef = useRef<WebSocket | undefined>(undefined); const socketRef = useRef<WebSocket | undefined>(undefined);
const retryRef = useRef(2);
const retryRef = useRef(2); // 等待 2 秒,重试 2 次


useEffect(() => { useEffect(() => {
scrollToBottom(false); scrollToBottom(false);
@@ -142,11 +142,12 @@ function LogGroup({
); );


socket.addEventListener('open', () => { socket.addEventListener('open', () => {
// console.log('WebSocket is open now.');
console.log('WebSocket is open now.');
}); });


socket.addEventListener('close', (event) => { socket.addEventListener('close', (event) => {
// console.log('WebSocket is closed:', event);
console.log('WebSocket is closed:', event);
// 有时候会出现连接失败,重试 2 次
if (event.code !== 1000 && retryRef.current > 0) { if (event.code !== 1000 && retryRef.current > 0) {
retryRef.current -= 1; retryRef.current -= 1;
setTimeout(() => { setTimeout(() => {
@@ -160,6 +161,7 @@ function LogGroup({
}); });


socket.addEventListener('message', (event) => { socket.addEventListener('message', (event) => {
console.log('message received.', event);
if (!event.data) { if (!event.data) {
return; return;
} }


+ 2
- 1
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -32,8 +32,9 @@ function LogList({
}: LogListProps) { }: LogListProps) {
const [logList, setLogList] = useState<ExperimentLog[]>([]); const [logList, setLogList] = useState<ExperimentLog[]>([]);
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
const retryRef = useRef(3);
const retryRef = useRef(3); // 等待 2 秒,重试 3 次


// 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志
useEffect(() => { useEffect(() => {
if ( if (
instanceNodeStatus && instanceNodeStatus &&


+ 29
- 0
react-ui/src/pages/Model/components/MetricsChart/index.less View File

@@ -0,0 +1,29 @@
.metrics-chart {
width: calc((100% - 30px) / 3);
background-color: white;

&__title {
display: flex;
align-items: center;
height: 36px;
padding-left: 15px;
color: @text-color;
font-size: 14px;
background-color: #ebf2ff;

img {
width: 13px;
height: 13px;
margin-right: 12px;
}
}

&__chart {
width: 100%;
height: 280px;
background: linear-gradient(180deg, #ffffff 0%, #fdfeff 100%);
border: 1px solid white;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
}
}

+ 174
- 0
react-ui/src/pages/Model/components/MetricsChart/index.tsx View File

@@ -0,0 +1,174 @@
import * as echarts from 'echarts';
import { useEffect, useRef } from 'react';
import styles from './index.less';
import './tooltip.css';

const colors = [
'#0D5EF8',
'#6AC21D',
'#F98E1B',
'#ECB934',
'#8A34EC',
'#FF1493',
'#FFFF00',
'#DAA520',
'#CD853F',
'#FF6347',
'#808080',
'#00BFFF',
'#008000',
'#00FFFF',
'#FFFACD',
'#FFA500',
'#FF4500',
'#800080',
'#FF1493',
'#000080',
];

const backgroundColor = new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: '#ffffff' },
{ offset: 1, color: '#fdfeff' },
],
false,
);

function getTooltip(xTitle: string, xValue: number, yTitle: string, yValue: number) {
const str = `<div class="metrics-tooltip">
<span class="y-text">Y:</span>
<span class="x-text">X:</span>
<div class="title">${yTitle}</div>
<div class="value">${yValue}</div>
<div class="title" style="margin-top: 10px">${xTitle}</div>
<div class="value">${xValue}</div>
<div>`;
return str;
}

export type MetricsChatData = {
name: string;
values: number[];
version: string;
iters: number[];
};

export type MetricsChartProps = {
name: string;
chartData: MetricsChatData[];
};

function MetricsChart({ name, chartData }: MetricsChartProps) {
const chartRef = useRef<HTMLDivElement>(null);
const xAxisData = chartData[0]?.iters;
const seriesData = chartData.map((item) => {
return {
name: item.version,
type: 'line' as const,
smooth: true,
data: item.values,
};
});

const options: echarts.EChartsOption = {
backgroundColor: backgroundColor,
title: {
show: false,
},
tooltip: {
trigger: 'item',
padding: 10,
formatter: (params: any) => {
const { name: xTitle, data } = params;
return getTooltip('step', xTitle, name, data);
},
},
legend: {
bottom: 10,
icon: 'rect',
itemWidth: 10,
itemHeight: 10,
itemGap: 20,
textStyle: {
color: 'rgba(29, 29, 32, 0.75)',
fontSize: 12,
},
},
color: colors,
grid: {
left: '15',
right: '15',
top: '20',
bottom: '60',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: true,
offset: 10,
data: xAxisData,
axisLabel: {
color: 'rgba(29, 29, 32, 0.75)',
fontSize: 12,
},
axisTick: {
show: false,
},
axisLine: {
lineStyle: {
color: '#eaeaea',
width: 1,
},
},
},
yAxis: {
type: 'value',
axisLabel: {
color: 'rgba(29, 29, 32, 0.75)',
fontSize: 12,
margin: 15,
},
axisLine: {
show: false,
},
splitLine: {
lineStyle: {
color: '#e4e4e4',
width: 1,
type: 'dashed',
},
},
},
series: seriesData,
};

useEffect(() => {
// 创建一个echarts实例,返回echarts实例
const chart = echarts.init(chartRef.current);

// 设置图表实例的配置项和数据
chart.setOption(options);

// 组件卸载
return () => {
// myChart.dispose() 销毁实例
chart.dispose();
};
}, []);

return (
<div className={styles['metrics-chart']}>
<div className={styles['metrics-chart__title']}>
<img src={require('@/assets/img/metrics-title-icon.png')}></img>
<span>{name}</span>
</div>
<div className={styles['metrics-chart__chart']} ref={chartRef}></div>
</div>
);
}

export default MetricsChart;

+ 33
- 0
react-ui/src/pages/Model/components/MetricsChart/tooltip.css View File

@@ -0,0 +1,33 @@
.metrics-tooltip {
width: 172px;
padding-left: 20px;
background-color: white;
font-size: 12px;
}

.metrics-tooltip .y-text {
position: absolute;
left: 10px;
top: 10px;
}

.metrics-tooltip .x-text {
position: absolute;
left: 10px;
top: 66px;
}

.metrics-tooltip .title {
color: #575757;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 3px;
}

.metrics-tooltip .value {
color: #1d1d20;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

+ 5
- 2
react-ui/src/pages/Model/components/ModelEvolution/index.less View File

@@ -1,11 +1,14 @@
.model-evolution { .model-evolution {
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0 30px 20px;
overflow-x: hidden; overflow-x: hidden;
background-color: white;
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);


&__graph { &__graph {
height: calc(100%);
height: 100%;
background-color: @background-color; background-color: @background-color;
background-image: url(@/assets/img/pipeline-canvas-bg.png); background-image: url(@/assets/img/pipeline-canvas-bg.png);
background-size: 100% 100%; background-size: 100% 100%;


+ 35
- 0
react-ui/src/pages/Model/components/ModelMetrics/index.less View File

@@ -0,0 +1,35 @@
.model-metrics {
&__table {
margin-top: 10px;
padding: 20px 30px 0;
background: white;
border-radius: 10px;

:global {
.ant-table-container {
border: none !important;
}
.ant-table-thead {
.ant-table-cell {
background-color: rgb(247, 247, 247);
border-color: @border-color-base !important;
}
}
.ant-table-tbody {
.ant-table-cell {
border-right: none !important;
border-left: none !important;
}
}
}
}

&__chart {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
width: 100%;
margin-top: 10px;
}
}

+ 259
- 0
react-ui/src/pages/Model/components/ModelMetrics/index.tsx View File

@@ -0,0 +1,259 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import { useCheck } from '@/hooks';
import { getModelPageVersionsReq, getModelVersionsMetricsReq } from '@/services/dataset';
import { to } from '@/utils/promise';
import tableCellRender from '@/utils/table';
import { Checkbox, Table, Tooltip, type TablePaginationConfig, type TableProps } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import MetricsChart, { MetricsChatData } from '../MetricsChart';
import styles from './index.less';

enum MetricsType {
Train = 'train', // 训练
Evaluate = 'evaluate', // 评估
}

type TableData = {
name: string;
metrics_names?: string[];
metrics?: Record<string, number>;
params_names?: string[];
params?: Record<string, string>;
};

type ModelMetricsProps = {
resourceId: number;
identifier: string;
owner: string;
version: string;
};

function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsProps) {
const [pagination, setPagination] = useState<TablePaginationConfig>({
current: 1,
pageSize: 10,
});
const [total, setTotal] = useState(0);
const [tableData, setTableData] = useState<TableData[]>([]);
const [chartData, setChartData] = useState<Record<string, MetricsChatData[]> | undefined>(
undefined,
);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
// 获取所有的指标名称
const allMetricsNames = useMemo(() => {
const first: TableData | undefined = tableData.find(
(item) => item.metrics_names && item.metrics_names.length > 0,
);
return first?.metrics_names ?? [];
}, [tableData]);
const [
selectedMetrics,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_setSelectedMetrics,
metricsChecked,
metricsIndeterminate,
checkAllMetrics,
isSingleMetricsChecked,
checkSingleMetrics,
] = useCheck(allMetricsNames);

useEffect(() => {
getModelPageVersions();
}, []);

useEffect(() => {
if (selectedMetrics.length !== 0 && selectedRowKeys.length !== 0) {
getModelVersionsMetrics();
} else {
setChartData(undefined);
}
}, [selectedMetrics, selectedRowKeys]);

useEffect(() => {
const curRow = tableData.find((item) => item.name === version);
if (
curRow &&
curRow.metrics_names &&
curRow.metrics_names.length > 0 &&
!selectedRowKeys.includes(version)
) {
setSelectedRowKeys([version, ...selectedRowKeys]);
}
}, [version]);

// 获取模型版本列表,带有参数和指标数据
const getModelPageVersions = async () => {
const params = {
page: pagination.current! - 1,
size: pagination.pageSize,
identifier: identifier,
owner: owner,
type: MetricsType.Train,
};
const [res] = await to(getModelPageVersionsReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

const getModelVersionsMetrics = async () => {
const params = {
versions: selectedRowKeys,
metrics: selectedMetrics,
type: MetricsType.Train,
identifier: identifier,
repo_id: resourceId,
};
const [res] = await to(getModelVersionsMetricsReq(params));
if (res && res.data) {
setChartData(res.data);
}
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => {
if (action === 'paginate') {
setPagination(pagination);
}
// console.log(pagination, filters, sorter, action);
};

const rowSelection: TableProps['rowSelection'] = {
type: 'checkbox',
fixed: 'left',
selectedRowKeys,
onChange: (selectedRowKeys: React.Key[]) => {
setSelectedRowKeys(selectedRowKeys);
},
getCheckboxProps: (record: TableData) => ({
disabled: !record.metrics_names || record.metrics_names.length === 0,
}),
};

const showTableData = useMemo(() => {
const index = tableData.findIndex((item) => item.name === version);
if (index !== -1) {
const rowData = tableData[index];
const newTableData = tableData.filter((_, idx) => idx !== index);
return [rowData, ...newTableData];
}
}, [version, tableData]);

// 表头
const columns: TableProps['columns'] = useMemo(() => {
const first: TableData | undefined = tableData.find(
(item) => item.metrics_names && item.metrics_names.length > 0,
);
return [
{
title: '基本信息',
align: 'center',
children: [
{
title: '版本号',
dataIndex: 'name',
key: 'name',
width: 180,
fixed: 'left',
align: 'center',
render: tableCellRender(false),
},
],
},
{
title: `训练参数`,
align: 'center',
children: first?.params_names?.map((name) => ({
title: (
<Tooltip title={name}>
<span>{name}</span>
</Tooltip>
),
dataIndex: ['params', name],
key: name,
width: 120,
align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
sorter: (a, b) => a.params?.[name] ?? 0 - b.params?.[name] ?? 0,
showSorterTooltip: false,
})),
},
{
title: () => (
<div>
<Checkbox
checked={metricsChecked}
indeterminate={metricsIndeterminate}
onChange={checkAllMetrics}
></Checkbox>{' '}
<span>训练指标</span>
</div>
),
align: 'center',
children: first?.metrics_names?.map((name) => ({
title: (
<div>
<Checkbox
checked={isSingleMetricsChecked(name)}
onChange={(e) => {
e.stopPropagation();
checkSingleMetrics(name);
}}
onClick={(e) => e.stopPropagation()}
></Checkbox>{' '}
<Tooltip title={name}>
<span>{name}</span>
</Tooltip>
</div>
),
dataIndex: ['metrics', name],
key: name,
width: 120,
align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
sorter: (a, b) => a.metrics?.[name] ?? 0 - b.metrics?.[name] ?? 0,
showSorterTooltip: false,
})),
},
];
}, [tableData, selectedMetrics]);

return (
<div className={styles['model-metrics']}>
<div className={styles['model-metrics__table']}>
<SubAreaTitle
title="指标参数差异对比"
image={require('@/assets/img/model-metrics.png')}
style={{ marginBottom: '15px' }}
></SubAreaTitle>
<Table
dataSource={showTableData}
columns={columns}
rowSelection={rowSelection}
bordered={true}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="name"
/>
</div>
<div className={styles['model-metrics__chart']}>
{chartData &&
Object.keys(chartData).map((key) => (
<MetricsChart key={key} name={key} chartData={chartData[key]}></MetricsChart>
))}
</div>
</div>
);
}

export default ModelMetrics;

+ 16
- 0
react-ui/src/services/dataset/index.js View File

@@ -149,4 +149,20 @@ export function exportModelReq(data) {
method: 'POST', method: 'POST',
data data
}); });
}

// 分页查询模型所有版本,带有参数和指标数据
export function getModelPageVersionsReq(params) {
return request(`/api/mmp/newmodel/queryVersions`, {
method: 'GET',
params
});
}

// 获取模型版本指标对比
export function getModelVersionsMetricsReq(data) {
return request(`/api/mmp/newmodel/queryVersionsMetrics`, {
method: 'POST',
data
});
} }

+ 5
- 2
react-ui/src/services/experiment/index.js View File

@@ -59,6 +59,7 @@ export function getQueryByExperimentLog(data) {
method: 'POST', method: 'POST',
data, data,
skipErrorHandler: true, skipErrorHandler: true,
skipLoading: true,
}); });
} }
// 查询实例节点结果 // 查询实例节点结果
@@ -128,16 +129,18 @@ export function getTensorBoardStatusReq(data) {
} }


// 获取当前实验的模型推理指标信息 // 获取当前实验的模型推理指标信息
export function getExpEvaluateInfosReq(experimentId) {
export function getExpEvaluateInfosReq(experimentId, params) {
return request(`/api/mmp/aim/getExpEvaluateInfos/${experimentId}`, { return request(`/api/mmp/aim/getExpEvaluateInfos/${experimentId}`, {
method: 'GET', method: 'GET',
params
}); });
} }


// 获取当前实验的模型训练指标信息 // 获取当前实验的模型训练指标信息
export function getExpTrainInfosReq(experimentId) {
export function getExpTrainInfosReq(experimentId, params) {
return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, { return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, {
method: 'GET', method: 'GET',
params
}); });
} }




Loading…
Cancel
Save