| @@ -5,48 +5,54 @@ | |||
| gap: 20px 40px; | |||
| align-items: flex-start; | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| import { Link } from '@umijs/max'; | |||
| import { Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| @@ -11,6 +12,7 @@ export type BasicInfoLink = { | |||
| export type BasicInfoData = { | |||
| label: string; | |||
| value?: any; | |||
| ellipsis?: boolean; | |||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||
| }; | |||
| @@ -18,45 +20,73 @@ type BasicInfoProps = { | |||
| datas: BasicInfoData[]; | |||
| className?: string; | |||
| 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 ( | |||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||
| {datas.map((item) => ( | |||
| <BasicInfoItem key={item.label} data={item} labelWidth={labelWidth} /> | |||
| <BasicInfoItem | |||
| key={item.label} | |||
| data={item} | |||
| labelWidth={labelWidth} | |||
| classPrefix="kf-basic-info" | |||
| /> | |||
| ))} | |||
| </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 myClassName = `${classPrefix}__item`; | |||
| let valueComponent = undefined; | |||
| if (Array.isArray(formatValue)) { | |||
| valueComponent = ( | |||
| <div className="kf-basic-info-item__list-value"> | |||
| <div className={`${myClassName}__value-container`}> | |||
| {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> | |||
| ); | |||
| } else if (typeof formatValue === 'object' && formatValue) { | |||
| 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 { | |||
| valueComponent = <BasicInfoItemValue value={formatValue} />; | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } | |||
| 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} | |||
| </div> | |||
| {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) { | |||
| 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} | |||
| </a> | |||
| ); | |||
| } 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} | |||
| </Link> | |||
| ); | |||
| } 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> | |||
| ); | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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> | |||
| ); | |||
| } | |||
| @@ -38,10 +38,6 @@ | |||
| &__bottom { | |||
| position: relative; | |||
| 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 { | |||
| position: absolute; | |||
| @@ -52,6 +48,12 @@ | |||
| :global { | |||
| .ant-tabs { | |||
| 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 { | |||
| height: 100%; | |||
| .ant-tabs-content { | |||
| @@ -164,7 +164,16 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| key: ResourceInfoTabKeys.Introduction, | |||
| label: `${typeName}简介`, | |||
| 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, | |||
| @@ -1,10 +1,25 @@ | |||
| .resource-intro { | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| import BasicInfo, { BasicInfoData } from '@/components/BasicInfo'; | |||
| import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; | |||
| import { | |||
| @@ -8,13 +8,19 @@ import { | |||
| ProjectDependency, | |||
| ResourceType, | |||
| TrainTask, | |||
| resourceConfig, | |||
| } from '@/pages/Dataset/config'; | |||
| import ModelMetrics from '@/pages/Model/components/ModelMetrics'; | |||
| import { getGitUrl } from '@/utils'; | |||
| import styles from './index.less'; | |||
| type ResourceIntroProps = { | |||
| resourceType: ResourceType; | |||
| info: DatasetData | ModelData; | |||
| resourceId: number; | |||
| identifier: string; | |||
| owner: string; | |||
| version?: string; | |||
| }; | |||
| 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) => { | |||
| if (!project || !project.url || !project.branch) { | |||
| return undefined; | |||
| @@ -93,49 +76,50 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | |||
| { | |||
| label: '数据集名称', | |||
| value: data.name, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| value: data.version, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '创建人', | |||
| value: data.create_by, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '更新时间', | |||
| value: data.update_time, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '数据来源', | |||
| value: data.dataset_source, | |||
| format: formatSource, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练任务', | |||
| value: data.train_task, | |||
| format: formatTrainTask, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '处理代码', | |||
| value: data.processing_code, | |||
| format: formatProject, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '数据集分类', | |||
| value: data.data_type, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '研究方向', | |||
| 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: '模型名称', | |||
| value: data.name, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| value: data.version, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '创建人', | |||
| value: data.create_by, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '更新时间', | |||
| value: data.update_time, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练镜像', | |||
| value: data.image, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练代码', | |||
| value: data.project_depency, | |||
| format: formatProject, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练数据集', | |||
| value: data.train_datasets, | |||
| format: formatDataset, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '测试数据集', | |||
| value: data.test_datasets, | |||
| format: formatDataset, | |||
| }, | |||
| { | |||
| label: '参数', | |||
| value: data.params, | |||
| format: formatParams, | |||
| }, | |||
| { | |||
| label: '指标', | |||
| value: data.metrics, | |||
| format: formatMetrics, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '模型来源', | |||
| value: data.model_source, | |||
| format: formatSource, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练任务', | |||
| value: data.train_task, | |||
| format: formatTrainTask, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '模型框架', | |||
| value: data.model_type, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '模型能力', | |||
| 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[] = | |||
| resourceType === ResourceType.Dataset | |||
| ? getDatasetDatas(info as DatasetData) | |||
| @@ -221,23 +207,37 @@ function ResourceIntro({ resourceType, info }: ResourceIntroProps) { | |||
| return ( | |||
| <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> | |||
| <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> | |||
| ); | |||
| } | |||
| @@ -1,4 +1,9 @@ | |||
| .resource-version { | |||
| min-height: 100%; | |||
| padding: 20px 30px; | |||
| color: @text-color; | |||
| font-size: @font-size-content; | |||
| background: white; | |||
| border-radius: 0 0 10px 10px; | |||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| } | |||
| @@ -86,7 +86,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| return ( | |||
| <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"> | |||
| <Button | |||
| type="default" | |||
| @@ -53,7 +53,7 @@ function ExperimentComparison() { | |||
| // setLoading(true); | |||
| const request = | |||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | |||
| const [res] = await to(request(experimentId)); | |||
| const [res] = await to(request(experimentId, { offset: '', limit: 50 })); | |||
| // setLoading(false); | |||
| if (res && res.data) { | |||
| // const { content = [], totalElements = 0 } = res.data; | |||
| @@ -202,9 +202,11 @@ function ExperimentComparison() { | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| rowSelection={rowSelection} | |||
| scroll={{ y: 'calc(100% - 55px)', x: '100%' }} | |||
| scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | |||
| pagination={false} | |||
| bordered={true} | |||
| virtual | |||
| // onScroll={handleTableScroll} | |||
| // loading={loading} | |||
| // pagination={{ | |||
| // ...pagination, | |||
| @@ -52,7 +52,7 @@ function LogGroup({ | |||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const socketRef = useRef<WebSocket | undefined>(undefined); | |||
| const retryRef = useRef(2); | |||
| const retryRef = useRef(2); // 等待 2 秒,重试 2 次 | |||
| useEffect(() => { | |||
| scrollToBottom(false); | |||
| @@ -142,11 +142,12 @@ function LogGroup({ | |||
| ); | |||
| socket.addEventListener('open', () => { | |||
| // console.log('WebSocket is open now.'); | |||
| console.log('WebSocket is open now.'); | |||
| }); | |||
| socket.addEventListener('close', (event) => { | |||
| // console.log('WebSocket is closed:', event); | |||
| console.log('WebSocket is closed:', event); | |||
| // 有时候会出现连接失败,重试 2 次 | |||
| if (event.code !== 1000 && retryRef.current > 0) { | |||
| retryRef.current -= 1; | |||
| setTimeout(() => { | |||
| @@ -160,6 +161,7 @@ function LogGroup({ | |||
| }); | |||
| socket.addEventListener('message', (event) => { | |||
| console.log('message received.', event); | |||
| if (!event.data) { | |||
| return; | |||
| } | |||
| @@ -32,8 +32,9 @@ function LogList({ | |||
| }: LogListProps) { | |||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const retryRef = useRef(3); | |||
| const retryRef = useRef(3); // 等待 2 秒,重试 3 次 | |||
| // 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志 | |||
| useEffect(() => { | |||
| if ( | |||
| instanceNodeStatus && | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -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; | |||
| } | |||
| @@ -1,11 +1,14 @@ | |||
| .model-evolution { | |||
| width: 100%; | |||
| height: 100%; | |||
| padding: 0 30px 20px; | |||
| 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 { | |||
| height: calc(100%); | |||
| height: 100%; | |||
| background-color: @background-color; | |||
| background-image: url(@/assets/img/pipeline-canvas-bg.png); | |||
| background-size: 100% 100%; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -149,4 +149,20 @@ export function exportModelReq(data) { | |||
| method: 'POST', | |||
| 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 | |||
| }); | |||
| } | |||
| @@ -59,6 +59,7 @@ export function getQueryByExperimentLog(data) { | |||
| method: 'POST', | |||
| data, | |||
| 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}`, { | |||
| method: 'GET', | |||
| params | |||
| }); | |||
| } | |||
| // 获取当前实验的模型训练指标信息 | |||
| export function getExpTrainInfosReq(experimentId) { | |||
| export function getExpTrainInfosReq(experimentId, params) { | |||
| return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, { | |||
| method: 'GET', | |||
| params | |||
| }); | |||
| } | |||