| @@ -0,0 +1,14 @@ | |||||
| .kf-info-group { | |||||
| width: 100%; | |||||
| &__content { | |||||
| padding: 20px @content-padding; | |||||
| background-color: white; | |||||
| border: 1px solid @border-color-base; | |||||
| border-radius: 0 0 4px 4px; | |||||
| &&--scroll { | |||||
| padding: 0; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,28 @@ | |||||
| import classNames from 'classnames'; | |||||
| import InfoGroupTitle from '../InfoGroupTitle'; | |||||
| import './index.less'; | |||||
| type InfoGroupProps = { | |||||
| title: string; | |||||
| contentScroll?: boolean; // 内容是否需要滚动,如果可以滚动,则取消 padding | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| children?: React.ReactNode; | |||||
| }; | |||||
| function InfoGroup({ title, contentScroll = false, className, style, children }: InfoGroupProps) { | |||||
| return ( | |||||
| <div className={classNames('kf-info-group', className)} style={style}> | |||||
| <InfoGroupTitle title={title} /> | |||||
| <div | |||||
| className={classNames('kf-info-group__content', { | |||||
| 'kf-info-group__content--scroll': contentScroll, | |||||
| })} | |||||
| > | |||||
| {children} | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default InfoGroup; | |||||
| @@ -1,4 +1,4 @@ | |||||
| .config-title { | |||||
| .kf-info-group-title { | |||||
| width: 100%; | width: 100%; | ||||
| height: 56px; | height: 56px; | ||||
| padding-left: @content-padding; | padding-left: @content-padding; | ||||
| @@ -10,7 +10,7 @@ | |||||
| border: 1px solid #e8effb; | border: 1px solid #e8effb; | ||||
| border-radius: 4px 4px 0 0; | border-radius: 4px 4px 0 0; | ||||
| &__img { | |||||
| &__image { | |||||
| width: 16px; | width: 16px; | ||||
| height: 16px; | height: 16px; | ||||
| margin-right: 10px; | margin-right: 10px; | ||||
| @@ -0,0 +1,25 @@ | |||||
| import { Flex } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import './index.less'; | |||||
| type InfoGroupTitleProps = { | |||||
| title: string; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| }; | |||||
| function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) { | |||||
| return ( | |||||
| <Flex align="center" className={classNames('kf-info-group-title', className)} style={style}> | |||||
| <img | |||||
| src={require('@/assets/img/code-name-icon.png')} | |||||
| className="kf-info-group-title__image" | |||||
| alt="" | |||||
| draggable={false} | |||||
| /> | |||||
| <span className="kf-info-group-title__text">{title}</span> | |||||
| </Flex> | |||||
| ); | |||||
| } | |||||
| export default InfoGroupTitle; | |||||
| @@ -33,7 +33,7 @@ function AutoMLInstance() { | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (instanceId) { | if (instanceId) { | ||||
| getExperimentInsInfo(); | |||||
| getExperimentInsInfo(false); | |||||
| } | } | ||||
| return () => { | return () => { | ||||
| closeSSE(); | closeSSE(); | ||||
| @@ -41,7 +41,7 @@ function AutoMLInstance() { | |||||
| }, []); | }, []); | ||||
| // 获取实验实例详情 | // 获取实验实例详情 | ||||
| const getExperimentInsInfo = async () => { | |||||
| const getExperimentInsInfo = async (isStatusDetermined: boolean) => { | |||||
| const [res] = await to(getExperimentInsReq(instanceId)); | const [res] = await to(getExperimentInsReq(instanceId)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const info = res.data as AutoMLInstanceData; | const info = res.data as AutoMLInstanceData; | ||||
| @@ -51,6 +51,17 @@ function AutoMLInstance() { | |||||
| if (paramJson) { | if (paramJson) { | ||||
| setAutoMLInfo(paramJson); | setAutoMLInfo(paramJson); | ||||
| } | } | ||||
| // 这个接口返回的状态有延时,SSE 返回的状态是最新的 | |||||
| // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE | |||||
| if (isStatusDetermined) { | |||||
| setInstanceInfo((prev) => ({ | |||||
| ...info, | |||||
| nodeStatus: prev!.nodeStatus, | |||||
| })); | |||||
| return; | |||||
| } | |||||
| // 进行节点状态 | // 进行节点状态 | ||||
| const nodeStatusJson = parseJsonText(node_status); | const nodeStatusJson = parseJsonText(node_status); | ||||
| if (nodeStatusJson) { | if (nodeStatusJson) { | ||||
| @@ -93,7 +104,7 @@ function AutoMLInstance() { | |||||
| ) as NodeStatus; | ) as NodeStatus; | ||||
| if (statusData) { | if (statusData) { | ||||
| setInstanceInfo((prev) => ({ | setInstanceInfo((prev) => ({ | ||||
| ...(prev as AutoMLInstanceData), | |||||
| ...prev!, | |||||
| nodeStatus: statusData, | nodeStatus: statusData, | ||||
| })); | })); | ||||
| @@ -103,7 +114,7 @@ function AutoMLInstance() { | |||||
| statusData.phase !== ExperimentStatus.Running | statusData.phase !== ExperimentStatus.Running | ||||
| ) { | ) { | ||||
| closeSSE(); | closeSSE(); | ||||
| getExperimentInsInfo(); | |||||
| getExperimentInsInfo(true); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -164,7 +175,11 @@ function AutoMLInstance() { | |||||
| label: '实验结果', | label: '实验结果', | ||||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | icon: <KFIcon type="icon-shiyanjieguo1" />, | ||||
| children: ( | children: ( | ||||
| <ExperimentResult fileUrl={instanceInfo?.result_path} imageUrl={instanceInfo?.img_path} /> | |||||
| <ExperimentResult | |||||
| fileUrl={instanceInfo?.result_path} | |||||
| imageUrl={instanceInfo?.img_path} | |||||
| modelPath={instanceInfo?.model_path} | |||||
| /> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -278,20 +278,19 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| return ( | return ( | ||||
| <div className={classNames(styles['auto-ml-basic'], className)}> | <div className={classNames(styles['auto-ml-basic'], className)}> | ||||
| {isInstance && runStatus ? ( | |||||
| {isInstance && runStatus && ( | |||||
| <ConfigInfo | <ConfigInfo | ||||
| title="运行信息" | title="运行信息" | ||||
| data={instanceDatas} | data={instanceDatas} | ||||
| labelWidth={70} | labelWidth={70} | ||||
| threeColumn | |||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| ) : ( | |||||
| )} | |||||
| {!isInstance && ( | |||||
| <ConfigInfo | <ConfigInfo | ||||
| title="基本信息" | title="基本信息" | ||||
| data={basicDatas} | data={basicDatas} | ||||
| labelWidth={70} | labelWidth={70} | ||||
| threeColumn | |||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| )} | )} | ||||
| @@ -299,10 +298,9 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| title="配置信息" | title="配置信息" | ||||
| data={configDatas} | data={configDatas} | ||||
| labelWidth={150} | labelWidth={150} | ||||
| threeColumn | |||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} threeColumn /> | |||||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} /> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,20 +1,10 @@ | |||||
| .config-info { | .config-info { | ||||
| flex: 1; | |||||
| min-width: 0; | |||||
| &__content { | |||||
| padding: 20px; | |||||
| padding: 20px @content-padding; | |||||
| background-color: white; | |||||
| border: 1px solid @border-color-base; | |||||
| border-radius: 0 0 4px 4px; | |||||
| } | |||||
| :global { | :global { | ||||
| .kf-basic-info { | .kf-basic-info { | ||||
| width: 100%; | width: 100%; | ||||
| &__item { | &__item { | ||||
| width: calc((100% - 80px) / 3); | |||||
| &__label { | &__label { | ||||
| font-size: @font-size; | font-size: @font-size; | ||||
| text-align: left; | text-align: left; | ||||
| @@ -27,14 +17,4 @@ | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| &--three-column { | |||||
| :global { | |||||
| .kf-basic-info { | |||||
| &__item { | |||||
| width: calc((100% - 80px) / 3); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -1,7 +1,6 @@ | |||||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | ||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect } from 'react'; | |||||
| import ConfigTitle from '../ConfigTitle'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export * from '@/components/BasicInfo/format'; | export * from '@/components/BasicInfo/format'; | ||||
| export type { BasicInfoData }; | export type { BasicInfoData }; | ||||
| @@ -9,39 +8,18 @@ export type { BasicInfoData }; | |||||
| type ConfigInfoProps = { | type ConfigInfoProps = { | ||||
| title: string; | title: string; | ||||
| data: BasicInfoData[]; | data: BasicInfoData[]; | ||||
| labelWidth: number; | |||||
| className?: string; | className?: string; | ||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| children?: React.ReactNode; | |||||
| labelWidth: number; | |||||
| threeColumn?: boolean; | |||||
| }; | }; | ||||
| function ConfigInfo({ | |||||
| title, | |||||
| data, | |||||
| className, | |||||
| style, | |||||
| children, | |||||
| labelWidth, | |||||
| threeColumn = false, | |||||
| }: ConfigInfoProps) { | |||||
| useEffect(() => {}, []); | |||||
| function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) { | |||||
| return ( | return ( | ||||
| <div | |||||
| className={classNames( | |||||
| styles['config-info'], | |||||
| { [styles['config-info--three-column']]: threeColumn }, | |||||
| className, | |||||
| )} | |||||
| style={style} | |||||
| > | |||||
| <ConfigTitle title={title} /> | |||||
| <InfoGroup title={title} className={classNames(styles['config-info'], className)} style={style}> | |||||
| <div className={styles['config-info__content']}> | <div className={styles['config-info__content']}> | ||||
| <BasicInfo datas={data} labelWidth={labelWidth} /> | <BasicInfo datas={data} labelWidth={labelWidth} /> | ||||
| {children} | |||||
| </div> | </div> | ||||
| </div> | |||||
| </InfoGroup> | |||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,22 +0,0 @@ | |||||
| import { Flex } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| type ConfigTitleProps = { | |||||
| title: string; | |||||
| }; | |||||
| function ConfigTitle({ title }: ConfigTitleProps) { | |||||
| return ( | |||||
| <Flex align="center" className={styles['config-title']}> | |||||
| <img | |||||
| src={require('@/assets/img/code-name-icon.png')} | |||||
| className={styles['config-title__img']} | |||||
| alt="" | |||||
| draggable={false} | |||||
| /> | |||||
| <span className={styles['config-title__text']}>{title}</span> | |||||
| </Flex> | |||||
| ); | |||||
| } | |||||
| export default ConfigTitle; | |||||
| @@ -6,34 +6,49 @@ | |||||
| background-color: white; | background-color: white; | ||||
| border-radius: 10px; | border-radius: 10px; | ||||
| &__download { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| height: 34px; | |||||
| margin-bottom: 16px; | |||||
| padding-left: @content-padding; | |||||
| color: @text-color; | |||||
| font-size: 13px; | |||||
| background-color: #f8f8f9; | |||||
| border-radius: 4px; | |||||
| &__btn { | |||||
| margin-left: 22px; | |||||
| } | |||||
| } | |||||
| &__text { | &__text { | ||||
| width: 100%; | width: 100%; | ||||
| height: 460px; | |||||
| margin-bottom: 16px; | |||||
| height: 420px; | |||||
| padding: 20px @content-padding; | padding: 20px @content-padding; | ||||
| overflow: auto; | overflow: auto; | ||||
| white-space: pre-wrap; | white-space: pre-wrap; | ||||
| border: 1px solid @border-color-base; | |||||
| border-radius: 0 0 4px 4px; | |||||
| } | } | ||||
| &__image-container { | |||||
| &__images { | |||||
| display: flex; | display: flex; | ||||
| align-items: flex-start; | align-items: flex-start; | ||||
| width: 100%; | width: 100%; | ||||
| padding: 20px @content-padding; | |||||
| overflow-x: auto; | overflow-x: auto; | ||||
| border: 1px solid @border-color-base; | |||||
| border-radius: 0 0 4px 4px; | |||||
| &__image { | |||||
| height: 248px; | |||||
| margin-right: 20px; | |||||
| border: 1px solid rgba(96, 107, 122, 0.3); | |||||
| :global { | |||||
| .ant-image { | |||||
| margin-right: 20px; | |||||
| &:last-child { | |||||
| margin-right: 0; | |||||
| &:last-child { | |||||
| margin-right: 0; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| &__item { | |||||
| height: 248px; | |||||
| border: 1px solid rgba(96, 107, 122, 0.3); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,15 +1,18 @@ | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { getFileReq } from '@/services/file'; | import { getFileReq } from '@/services/file'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { Button, Image } from 'antd'; | |||||
| import { useEffect, useMemo, useState } from 'react'; | import { useEffect, useMemo, useState } from 'react'; | ||||
| import ConfigTitle from '../ConfigTitle'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentResultProps = { | type ExperimentResultProps = { | ||||
| fileUrl?: string; | fileUrl?: string; | ||||
| imageUrl?: string; | imageUrl?: string; | ||||
| modelPath?: string; | |||||
| }; | }; | ||||
| function ExperimentResult({ fileUrl, imageUrl }: ExperimentResultProps) { | |||||
| function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { | |||||
| const [result, setResult] = useState<string | undefined>(''); | const [result, setResult] = useState<string | undefined>(''); | ||||
| const images = useMemo(() => { | const images = useMemo(() => { | ||||
| @@ -35,20 +38,48 @@ function ExperimentResult({ fileUrl, imageUrl }: ExperimentResultProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-result']}> | <div className={styles['experiment-result']}> | ||||
| <ConfigTitle title="实验结果"></ConfigTitle> | |||||
| <div className={styles['experiment-result__text']}>{result}</div> | |||||
| <ConfigTitle title="可视化结果"></ConfigTitle> | |||||
| <div className={styles['experiment-result__image-container']}> | |||||
| {images.map((item, index) => ( | |||||
| <img | |||||
| key={index} | |||||
| className={styles['experiment-result__image-container__image']} | |||||
| src={item} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| {modelPath && ( | |||||
| <div className={styles['experiment-result__download']}> | |||||
| <span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span> | |||||
| <span>save_model.joblib</span> | |||||
| <Button | |||||
| type="link" | |||||
| className={styles['experiment-result__download__btn']} | |||||
| icon={<KFIcon type="icon-a-xiazai1" />} | |||||
| size="small" | |||||
| iconPosition="end" | |||||
| onClick={() => { | |||||
| window.location.href = modelPath; | |||||
| }} | |||||
| > | |||||
| 模型下载 | |||||
| </Button> | |||||
| </div> | |||||
| )} | |||||
| <InfoGroup title="实验结果" contentScroll> | |||||
| <div className={styles['experiment-result__text']}>{result}</div> | |||||
| </InfoGroup> | |||||
| <InfoGroup title="可视化结果" style={{ marginTop: '16px' }}> | |||||
| <div className={styles['experiment-result__images']}> | |||||
| <Image.PreviewGroup | |||||
| preview={{ | |||||
| onChange: (current, prev) => | |||||
| console.log(`current index: ${current}, prev index: ${prev}`), | |||||
| }} | |||||
| > | |||||
| {images.map((item) => ( | |||||
| <Image | |||||
| key={item} | |||||
| className={styles['experiment-result__images__item']} | |||||
| src={item} | |||||
| height={248} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| ))} | |||||
| </Image.PreviewGroup> | |||||
| </div> | |||||
| </InfoGroup> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -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); // 等待 2 秒,重试 2 次 | |||||
| const retryRef = useRef(2); // 等待 2 秒,重试 3 次 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| scrollToBottom(false); | scrollToBottom(false); | ||||
| @@ -147,7 +147,7 @@ function LogGroup({ | |||||
| socket.addEventListener('close', (event) => { | socket.addEventListener('close', (event) => { | ||||
| console.log('WebSocket is closed:', event); | console.log('WebSocket is closed:', event); | ||||
| // 有时候会出现连接失败,重试 2 次 | |||||
| // 有时候会出现连接失败,重试 3 次 | |||||
| if (event.code !== 1000 && retryRef.current > 0) { | if (event.code !== 1000 && retryRef.current > 0) { | ||||
| retryRef.current -= 1; | retryRef.current -= 1; | ||||
| setTimeout(() => { | setTimeout(() => { | ||||