+ {logGroups.length > 0 ? (
+ logGroups.map((v) =>
)
) : (
暂无日志
)}
diff --git a/react-ui/src/pages/Experiment/components/ViewParamsModal/index.less b/react-ui/src/pages/Experiment/components/ViewParamsModal/index.less
index a2b0ded5..06c5a4f0 100644
--- a/react-ui/src/pages/Experiment/components/ViewParamsModal/index.less
+++ b/react-ui/src/pages/Experiment/components/ViewParamsModal/index.less
@@ -1,31 +1,14 @@
-.params_container {
- max-height: 230px;
- padding: 15px 15px 0;
+.params-container {
+ max-height: calc(100vh - 300px);
+ padding: 24px 24px 0;
overflow-y: auto;
border: 1px solid #e6e6e6;
border-radius: 8px;
-
- &_line {
- display: flex;
- align-items: center;
- margin-bottom: 15px;
-
- &_label {
- width: 180px;
- color: @text-color;
- font-size: 15px;
- }
- &_value {
- flex: 1;
- width: 100px;
- margin-left: 15px;
- padding: 10px 20px;
- color: @text-color;
- font-size: @font-size;
- line-height: 20px;
- background: #f6f6f6;
- border: 1px solid #e0e0e1;
- border-radius: 4px;
+}
+.params-empty {
+ :global {
+ .kf-empty__image {
+ width: 300px;
}
}
}
diff --git a/react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx b/react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx
index f860135a..8bd49817 100644
--- a/react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx
+++ b/react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx
@@ -4,9 +4,11 @@
* @Description: 查看实验使用的参数
*/
import parameterImg from '@/assets/img/modal-parameter.png';
+import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
-import { getParamType } from '../AddExperimentModal';
+import { Form } from 'antd';
+import { getParamComponent, getParamLabel } from '../AddExperimentModal';
import styles from './index.less';
type ParamsModalProps = {
@@ -26,14 +28,44 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
cancelButtonProps={{ style: { display: 'none' } }}
width={825}
>
-
- {globalParam?.map((item) => (
-
- {getParamType(item)}
- {item.param_value}
-
- ))}
-
+ {Array.isArray(globalParam) && globalParam.length > 0 ? (
+
+
+ {(fields) =>
+ fields.map(({ key, name, ...restField }) => (
+
+ {getParamComponent(
+ globalParam[name]['param_type'],
+ globalParam[name]['is_sensitive'],
+ )}
+
+ ))
+ }
+
+
+
+ ) : (
+
+ )}
);
}
diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx
index de8504f5..1ecc7df1 100644
--- a/react-ui/src/pages/Experiment/index.jsx
+++ b/react-ui/src/pages/Experiment/index.jsx
@@ -1,7 +1,7 @@
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ExperimentStatus, TensorBoardStatus } from '@/enums';
-import { useCacheState } from '@/hooks/pageCacheState';
+import { useCacheState } from '@/hooks/useCacheState';
import {
deleteExperimentById,
getExperiment,
@@ -15,18 +15,20 @@ import {
} from '@/services/experiment/index.js';
import { getWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
+import { ExperimentCompleted } from '@/utils/constant';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { App, Button, ConfigProvider, Dropdown, Input, Space, Table, Tooltip } from 'antd';
import classNames from 'classnames';
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ComparisonType } from './Comparison/config';
import AddExperimentModal from './components/AddExperimentModal';
-import ExperimentInstance from './components/ExperimentInstance';
+import ExperimentInstanceList from './components/ExperimentInstanceList';
import styles from './index.less';
import { experimentStatusInfo } from './status';
+import { useServerTime } from '@/hooks/useServerTime';
// 定时器
const timerIds = new Map();
@@ -35,14 +37,8 @@ function Experiment() {
const navigate = useNavigate();
const [experimentList, setExperimentList] = useState([]);
const [workflowList, setWorkflowList] = useState([]);
- const [queryFlow, setQueryFlow] = useState({
- offset: 1,
- page: 0,
- size: 10000,
- name: null,
- });
const [experimentId, setExperimentId] = useState(null);
- const [experimentInList, setExperimentInList] = useState([]);
+ const [experimentInsList, setExperimentInsList] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState(null);
const [total, setTotal] = useState(0);
const [isAdd, setIsAdd] = useState(true);
@@ -52,6 +48,7 @@ function Experiment() {
const [cacheState, setCacheState] = useCacheState();
const [searchText, setSearchText] = useState(cacheState?.searchText);
const [inputText, setInputText] = useState(cacheState?.searchText);
+ const [now] = useServerTime();
const [pagination, setPagination] = useState(
cacheState?.pagination ?? {
current: 1,
@@ -59,20 +56,10 @@ function Experiment() {
},
);
const { message } = App.useApp();
-
- useEffect(() => {
- getWorkflowList();
- return () => {
- clearExperimentInTimers();
- };
- }, []);
-
- useEffect(() => {
- getExperimentList();
- }, [pagination, searchText]);
+ const timerRef = useRef();
// 获取实验列表
- const getExperimentList = async () => {
+ const getExperimentList = useCallback(async () => {
const params = {
page: pagination.current - 1,
size: pagination.pageSize,
@@ -88,15 +75,81 @@ function Experiment() {
setTotal(res.data.totalElements);
}
- };
+ }, [pagination, searchText]);
+
+ // 刷新实验列表状态,
+ // 目前是直接刷新实验列表,后续需要优化,只刷新状态
+ const refreshExperimentList = useCallback(() => {
+ getExperimentList();
+ }, [getExperimentList]);
// 获取流水线列表
- const getWorkflowList = async () => {
- const [res] = await to(getWorkflow(queryFlow));
- if (res && res.data && res.data.content) {
- setWorkflowList(res.data.content);
- }
- };
+ useEffect(() => {
+ // 获取流水线列表
+ const getWorkflowList = async () => {
+ const queryFlow = {
+ offset: 1,
+ page: 0,
+ size: 10000,
+ name: null,
+ };
+ const [res] = await to(getWorkflow(queryFlow));
+ if (res && res.data && res.data.content) {
+ setWorkflowList(res.data.content);
+ }
+ };
+
+ getWorkflowList();
+ return () => {
+ clearExperimentInTimers();
+ };
+ }, []);
+
+ // 获取实验列表
+ useEffect(() => {
+ getExperimentList();
+ }, [getExperimentList]);
+
+ // 新增,删除版本时,重置分页,然后刷新版本列表
+ useEffect(() => {
+ const handleMessage = (e) => {
+ const { type, payload } = e.data;
+ if (type === ExperimentCompleted) {
+ const { id, status, finish_time } = payload;
+
+ // 修改实例的状态和结束时间
+ setExperimentInsList((prev) =>
+ prev.map((v) =>
+ v.id === id
+ ? {
+ ...v,
+ status: status,
+ finish_time: finish_time,
+ }
+ : v,
+ ),
+ );
+
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = undefined;
+ }
+
+ timerRef.current = setTimeout(() => {
+ refreshExperimentList();
+ }, 10000);
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+ return () => {
+ window.removeEventListener('message', handleMessage);
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = undefined;
+ }
+ };
+ }, [refreshExperimentList]);
// 搜索
const onSearch = (value) => {
@@ -108,11 +161,11 @@ function Experiment() {
};
// 获取实验实例列表
- const getQueryByExperiment = async (experimentId, page) => {
+ const getQueryByExperiment = async (experimentId, page, size = 5) => {
const params = {
experimentId: experimentId,
page: page,
- size: 5,
+ size: size,
};
const [res, error] = await to(getQueryByExperimentId(params));
if (res && res.data) {
@@ -127,10 +180,10 @@ function Experiment() {
};
});
if (page === 0) {
- setExperimentInList(list);
+ setExperimentInsList(list);
clearExperimentInTimers();
} else {
- setExperimentInList((prev) => [...prev, ...list]);
+ setExperimentInsList((prev) => [...prev, ...list]);
}
setExperimentInsTotal(totalElements);
// 获取 TensorBoard 状态
@@ -173,7 +226,7 @@ function Experiment() {
};
const [res] = await to(getTensorBoardStatusReq(params));
if (res && res.data) {
- setExperimentInList((prevList) => {
+ setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
return {
@@ -201,11 +254,12 @@ function Experiment() {
// 展开实例
const expandChange = (e, record) => {
clearExperimentInTimers();
- setExperimentInList([]);
+ setExperimentInsList([]);
if (record.id === expandedRowKeys) {
setExpandedRowKeys(null);
} else {
- getQueryByExperiment(record.id, 0);
+ getQueryByExperiment(record.id, 0, 5);
+ refreshExperimentList();
}
};
@@ -277,7 +331,6 @@ function Experiment() {
current,
pageSize,
});
- getExperimentList();
};
// 运行实验
const runExperiment = async (id) => {
@@ -285,9 +338,7 @@ function Experiment() {
if (res) {
message.success('运行成功');
refreshExperimentList();
- refreshExperimentIns(id);
- } else {
- message.error('运行失败');
+ getQueryByExperiment(id, 0, 5);
}
};
@@ -325,22 +376,17 @@ function Experiment() {
}
};
- // 刷新实验列表状态,
- // 目前是直接刷新实验列表,后续需要优化,只刷新状态
- const refreshExperimentList = () => {
- getExperimentList();
- };
-
// 实验实例终止
const handleInstanceTerminate = async (experimentIn) => {
// 刷新实验列表
refreshExperimentList();
- setExperimentInList((prevList) => {
+ setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
return {
...item,
status: ExperimentStatus.Terminated,
+ finish_time: now().toISOString(),
};
}
return item;
@@ -369,13 +415,39 @@ function Experiment() {
// 刷新实验实例列表
const refreshExperimentIns = (experimentId) => {
- getQueryByExperiment(experimentId, 0);
+ const length = experimentInsList.length;
+ getQueryByExperiment(experimentId, 0, length);
};
// 加载更多实验实例
const loadMoreExperimentIns = () => {
- const page = Math.round(experimentInList.length / 5);
- getQueryByExperiment(expandedRowKeys, page);
+ const page = Math.round(experimentInsList.length / 5);
+ getQueryByExperiment(expandedRowKeys, page, 5);
+ };
+
+ // 处理删除
+ const handleExperimentDelete = (record) => {
+ modalConfirm({
+ title: '删除后,该实验将不可恢复',
+ content: '是否确认删除?',
+ onOk: () => {
+ deleteExperimentById(record.id).then((ret) => {
+ if (ret.code === 200) {
+ message.success('删除成功');
+ // 如果是一页的唯一数据,删除后,请求第一页的数据
+ // 否则直接刷新这一页的数据
+ setPagination((prev) => {
+ return {
+ ...prev,
+ current: experimentList.length === 1 ? Math.max(1, prev.current - 1) : prev.current,
+ };
+ });
+ } else {
+ message.error(ret.msg);
+ }
+ });
+ },
+ });
};
const columns = [
@@ -383,7 +455,7 @@ function Experiment() {
title: '实验名称',
dataIndex: 'name',
key: 'name',
- render: tableCellRender(),
+ render: tableCellRender(false),
width: '16%',
},
{
@@ -400,7 +472,6 @@ function Experiment() {
dataIndex: 'description',
key: 'description',
render: tableCellRender(true),
- ellipsis: { showTitle: false },
},
{
title: '最近五次运行状态',
@@ -477,22 +548,7 @@ function Experiment() {
size="small"
key="batchRemove"
icon={
}
- onClick={() => {
- modalConfirm({
- title: '删除后,该实验将不可恢复',
- content: '是否确认删除?',
- onOk: () => {
- deleteExperimentById(record.id).then((ret) => {
- if (ret.code === 200) {
- message.success('删除成功');
- getExperimentList();
- } else {
- message.error(ret.msg);
- }
- });
- },
- });
- }}
+ onClick={() => handleExperimentDelete(record)}
>
删除
@@ -501,6 +557,7 @@ function Experiment() {
),
},
];
+
return (
@@ -536,8 +593,8 @@ function Experiment() {
scroll={{ y: 'calc(100% - 55px)' }}
expandable={{
expandedRowRender: (record) => (
-
gotoInstanceInfo(item, record)}
onClickTensorBoard={handleTensorboard}
@@ -547,13 +604,10 @@ function Experiment() {
}}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
- >
+ >
),
- onExpand: (e, a) => {
- expandChange(e, a);
- },
+ onExpand: expandChange,
expandedRowKeys: [expandedRowKeys],
- rowExpandable: (record) => true,
}}
/>
diff --git a/react-ui/src/pages/HyperParameter/Create/index.less b/react-ui/src/pages/HyperParameter/Create/index.less
new file mode 100644
index 00000000..ae065195
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/Create/index.less
@@ -0,0 +1,50 @@
+.create-hyper-parameter {
+ height: 100%;
+
+ &__content {
+ height: calc(100% - 60px);
+ margin-top: 10px;
+ padding: 30px 30px 10px;
+ overflow: auto;
+ color: @text-color;
+ font-size: @font-size-content;
+ background-color: white;
+ border-radius: 10px;
+
+ :global {
+ .ant-input-number {
+ width: 100%;
+ }
+
+ .ant-form-item {
+ margin-bottom: 20px;
+ }
+
+ .image-url {
+ margin-top: -15px;
+ .ant-form-item-label > label::after {
+ content: '';
+ }
+ }
+
+ .ant-btn-variant-text:disabled {
+ color: @text-disabled-color;
+ }
+
+ .ant-btn-variant-text {
+ color: #565658;
+ }
+
+ .ant-btn.ant-btn-icon-only .anticon {
+ font-size: 20px;
+ }
+
+ .anticon-question-circle {
+ margin-top: -12px;
+ margin-left: 1px !important;
+ color: @text-color-tertiary !important;
+ font-size: 12px !important;
+ }
+ }
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx
new file mode 100644
index 00000000..3276a94e
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/Create/index.tsx
@@ -0,0 +1,167 @@
+/*
+ * @Author: 赵伟
+ * @Date: 2024-04-16 13:58:08
+ * @Description: 创建实验
+ */
+import PageTitle from '@/components/PageTitle';
+import { addRayReq, getRayInfoReq, updateRayReq } from '@/services/hyperParameter';
+import { safeInvoke } from '@/utils/functional';
+import { to } from '@/utils/promise';
+import { useLocation, useNavigate, useParams } from '@umijs/max';
+import { App, Button, Form } from 'antd';
+import { useEffect } from 'react';
+import BasicConfig from '../components/CreateForm/BasicConfig';
+import ExecuteConfig from '../components/CreateForm/ExecuteConfig';
+import { getReqParamName } from '../components/CreateForm/utils';
+import { FormData, HyperParameterData } from '../types';
+import styles from './index.less';
+
+function CreateHyperParameter() {
+ const navigate = useNavigate();
+ const [form] = Form.useForm();
+ const { message } = App.useApp();
+ const params = useParams();
+ const id = safeInvoke(Number)(params.id);
+ const { pathname } = useLocation();
+ const isCopy = pathname.includes('copy');
+
+ useEffect(() => {
+ // 获取服务详情
+ const getHyperParameterInfo = async (id: number) => {
+ const [res] = await to(getRayInfoReq({ id }));
+ if (res && res.data) {
+ const info: HyperParameterData = res.data;
+ const { name: name_str, parameters, points_to_evaluate, ...rest } = info;
+ const name = isCopy ? `${name_str}-copy` : name_str;
+ if (parameters && Array.isArray(parameters)) {
+ parameters.forEach((item) => {
+ const paramName = getReqParamName(item.type);
+ item.range = item[paramName];
+ item[paramName] = undefined;
+ });
+ }
+
+ const formData = {
+ ...rest,
+ name,
+ parameters,
+ points_to_evaluate: points_to_evaluate ?? [],
+ };
+
+ form.setFieldsValue(formData);
+ }
+ };
+
+ // 编辑,复制
+ if (id && !Number.isNaN(id)) {
+ getHyperParameterInfo(id);
+ }
+ }, [id, form, isCopy]);
+
+ // 创建、更新、复制实验
+ const createExperiment = async (formData: FormData) => {
+ // 按后台接口要求,修改参数表单数据结构,将 "value" 参数改为 "bounds"/"values"/"value"
+ const formParameters = formData['parameters'];
+
+ const parameters = formParameters.map((item) => {
+ const paramName = getReqParamName(item.type);
+ const range = item.range;
+ return {
+ ...item,
+ [paramName]: range,
+ range: undefined,
+ };
+ });
+
+ // 根据后台要求,修改表单数据
+ const object = {
+ ...formData,
+ parameters: parameters,
+ };
+
+ const params =
+ id && !isCopy
+ ? {
+ id: id,
+ ...object,
+ }
+ : object;
+
+ const request = id && !isCopy ? updateRayReq : addRayReq;
+ const [res] = await to(request(params));
+ if (res) {
+ message.success('操作成功');
+ navigate(-1);
+ }
+ };
+
+ // 提交
+ const handleSubmit = (values: FormData) => {
+ createExperiment(values);
+ };
+
+ // 取消
+ const cancel = () => {
+ navigate(-1);
+ };
+
+ let buttonText = '新建';
+ let title = '新建实验';
+ if (id) {
+ if (isCopy) {
+ title = '复制实验';
+ buttonText = '确定';
+ } else {
+ title = '编辑实验';
+ buttonText = '更新';
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default CreateHyperParameter;
diff --git a/react-ui/src/pages/HyperParameter/Info/index.less b/react-ui/src/pages/HyperParameter/Info/index.less
new file mode 100644
index 00000000..d0c2398a
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/Info/index.less
@@ -0,0 +1,40 @@
+.hyper-parameter-info {
+ position: relative;
+ height: 100%;
+ &__tabs {
+ height: 50px;
+ padding-left: 25px;
+ background-image: url(@/assets/img/page-title-bg.png);
+ background-repeat: no-repeat;
+ background-position: top center;
+ background-size: 100% 100%;
+ }
+
+ &__content {
+ height: calc(100% - 60px);
+ margin-top: 10px;
+ }
+
+ &__tips {
+ position: absolute;
+ top: 11px;
+ left: 256px;
+ padding: 3px 12px;
+ color: #565658;
+ font-size: @font-size-content;
+ background: .addAlpha(@primary-color, 0.09) [];
+ border-radius: 4px;
+
+ &::before {
+ position: absolute;
+ top: 10px;
+ left: -6px;
+ width: 0;
+ height: 0;
+ border-top: 4px solid transparent;
+ border-right: 6px solid .addAlpha(@primary-color, 0.09) [];
+ border-bottom: 4px solid transparent;
+ content: '';
+ }
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/Info/index.tsx b/react-ui/src/pages/HyperParameter/Info/index.tsx
new file mode 100644
index 00000000..6c5cdbf8
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/Info/index.tsx
@@ -0,0 +1,47 @@
+/*
+ * @Author: 赵伟
+ * @Date: 2024-04-16 13:58:08
+ * @Description: 自主机器学习详情
+ */
+import PageTitle from '@/components/PageTitle';
+import { getRayInfoReq } from '@/services/hyperParameter';
+import { safeInvoke } from '@/utils/functional';
+import { to } from '@/utils/promise';
+import { useParams } from '@umijs/max';
+import { useCallback, useEffect, useState } from 'react';
+import HyperParameterBasic from '../components/HyperParameterBasic';
+import { HyperParameterData } from '../types';
+import styles from './index.less';
+
+function HyperparameterInfo() {
+ const params = useParams();
+ const hyperparameterId = safeInvoke(Number)(params.id);
+ const [hyperparameterInfo, setHyperparameterInfo] = useState
(
+ undefined,
+ );
+
+ // 获取详情
+ const getHyperparameterInfo = useCallback(async () => {
+ const [res] = await to(getRayInfoReq({ id: hyperparameterId }));
+ if (res && res.data) {
+ setHyperparameterInfo(res.data);
+ }
+ }, [hyperparameterId]);
+
+ useEffect(() => {
+ if (hyperparameterId) {
+ getHyperparameterInfo();
+ }
+ }, [hyperparameterId, getHyperparameterInfo]);
+
+ return (
+
+ );
+}
+
+export default HyperparameterInfo;
diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less
new file mode 100644
index 00000000..9a2f8bfb
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/Instance/index.less
@@ -0,0 +1,42 @@
+.hyper-parameter-instance {
+ height: 100%;
+
+ &__tabs {
+ height: 100%;
+ :global {
+ .ant-tabs-nav-list {
+ width: 100%;
+ height: 50px;
+ padding-left: 15px;
+ background-image: url(@/assets/img/page-title-bg.png);
+ background-repeat: no-repeat;
+ background-position: top center;
+ background-size: 100% 100%;
+ }
+
+ .ant-tabs-content-holder {
+ height: calc(100% - 50px);
+ .ant-tabs-content {
+ height: 100%;
+ .ant-tabs-tabpane {
+ height: 100%;
+ }
+ }
+ }
+ }
+ }
+
+ &__basic {
+ height: calc(100% - 10px);
+ margin-top: 10px;
+ }
+
+ &__log {
+ height: calc(100% - 10px);
+ margin-top: 10px;
+ padding: 8px calc(@content-padding - 8px) 20px;
+ overflow-y: visible;
+ background-color: white;
+ border-radius: 10px;
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx
new file mode 100644
index 00000000..79905c36
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx
@@ -0,0 +1,215 @@
+import KFIcon from '@/components/KFIcon';
+import { ExperimentStatus } from '@/enums';
+import { getRayInsReq } from '@/services/hyperParameter';
+import { NodeStatus } from '@/types';
+import { parseJsonText } from '@/utils';
+import { safeInvoke } from '@/utils/functional';
+import { to } from '@/utils/promise';
+import { useParams } from '@umijs/max';
+import { Tabs } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import ExperimentHistory from '../components/ExperimentHistory';
+import ExperimentLog from '../components/ExperimentLog';
+import ExperimentResult from '../components/ExperimentResult';
+import HyperParameterBasic from '../components/HyperParameterBasic';
+import { HyperParameterData, HyperParameterInstanceData } from '../types';
+import styles from './index.less';
+
+enum TabKeys {
+ Params = 'params',
+ Log = 'log',
+ Result = 'result',
+ History = 'history',
+}
+
+const NodePrefix = 'workflow';
+
+function HyperParameterInstance() {
+ const [experimentInfo, setExperimentInfo] = useState(undefined);
+ const [instanceInfo, setInstanceInfo] = useState(
+ undefined,
+ );
+ // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态
+ const [workflowStatus, setWorkflowStatus] = useState(undefined);
+ const [nodes, setNodes] = useState | undefined>(undefined);
+ const params = useParams();
+ const instanceId = safeInvoke(Number)(params.id);
+ const evtSourceRef = useRef(null);
+
+ useEffect(() => {
+ if (instanceId) {
+ getExperimentInsInfo(false);
+ }
+ return () => {
+ closeSSE();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [instanceId]);
+
+ // 获取实验实例详情
+ const getExperimentInsInfo = async (isStatusDetermined: boolean) => {
+ const [res] = await to(getRayInsReq(instanceId));
+ if (res && res.data) {
+ const info = res.data as HyperParameterInstanceData;
+ const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time } = info;
+ // 解析配置参数
+ const paramJson = parseJsonText(param).data;
+ if (paramJson) {
+ // 实例详情返回的参数是字符串,需要转换
+ if (typeof paramJson.parameters === 'string') {
+ paramJson.parameters = parseJsonText(paramJson.parameters);
+ }
+ if (!Array.isArray(paramJson.parameters)) {
+ paramJson.parameters = [];
+ }
+
+ // 实例详情返回的运行参数是字符串,需要转换
+ if (typeof paramJson.points_to_evaluate === 'string') {
+ paramJson.points_to_evaluate = parseJsonText(paramJson.points_to_evaluate);
+ }
+ if (!Array.isArray(paramJson.points_to_evaluate)) {
+ paramJson.points_to_evaluate = [];
+ }
+ setExperimentInfo({
+ ...paramJson,
+ create_time,
+ });
+ }
+
+ setInstanceInfo(info);
+
+ // 这个接口返回的状态有延时,SSE 返回的状态是最新的
+ // SSE 调用时,不需要解析 node_status,也不要重新建立 SSE
+ if (isStatusDetermined) {
+ return;
+ }
+
+ // 进行节点状态
+ const nodeStatusJson = parseJsonText(node_status);
+ if (nodeStatusJson) {
+ setNodes(nodeStatusJson);
+ Object.keys(nodeStatusJson).some((key) => {
+ if (key.startsWith(NodePrefix)) {
+ const workflowStatus = nodeStatusJson[key];
+ setWorkflowStatus(workflowStatus);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ // 运行中或者等待中,开启 SSE
+ if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) {
+ setupSSE(argo_ins_name, argo_ins_ns);
+ }
+ }
+ };
+
+ const setupSSE = (name: string, namespace: string) => {
+ const { origin } = location;
+ const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
+ const evtSource = new EventSource(
+ `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
+ { withCredentials: false },
+ );
+ evtSource.onmessage = (event) => {
+ const data = event?.data;
+ if (!data) {
+ return;
+ }
+ const dataJson = parseJsonText(data);
+ if (dataJson) {
+ const nodes = dataJson?.result?.object?.status?.nodes;
+ if (nodes) {
+ // 节点
+ setNodes(nodes);
+
+ const workflowStatus = Object.values(nodes).find((node: any) =>
+ node.displayName.startsWith(NodePrefix),
+ ) as NodeStatus;
+
+ // 设置工作流状态
+ if (workflowStatus) {
+ setWorkflowStatus(workflowStatus);
+
+ // 实验结束,关闭 SSE
+ if (
+ workflowStatus.phase !== ExperimentStatus.Pending &&
+ workflowStatus.phase !== ExperimentStatus.Running
+ ) {
+ closeSSE();
+ getExperimentInsInfo(true);
+ }
+ }
+ }
+ }
+ };
+ evtSource.onerror = (error) => {
+ console.error('SSE error: ', error);
+ };
+
+ evtSourceRef.current = evtSource;
+ };
+
+ const closeSSE = () => {
+ if (evtSourceRef.current) {
+ evtSourceRef.current.close();
+ evtSourceRef.current = null;
+ }
+ };
+
+ const basicTabItems = [
+ {
+ key: TabKeys.Params,
+ label: '基本信息',
+ icon: ,
+ children: (
+
+ ),
+ },
+ {
+ key: TabKeys.Log,
+ label: '日志',
+ icon: ,
+ children: (
+
+ {instanceInfo && nodes && }
+
+ ),
+ },
+ ];
+
+ const resultTabItems = [
+ {
+ key: TabKeys.Result,
+ label: '实验结果',
+ icon: ,
+ children: ,
+ },
+ {
+ key: TabKeys.History,
+ label: '寻优列表',
+ icon: ,
+ children: ,
+ },
+ ];
+
+ const tabItems =
+ instanceInfo?.status === ExperimentStatus.Succeeded
+ ? [...basicTabItems, ...resultTabItems]
+ : basicTabItems;
+
+ return (
+
+
+
+ );
+}
+
+export default HyperParameterInstance;
diff --git a/react-ui/src/pages/HyperParameter/List/index.tsx b/react-ui/src/pages/HyperParameter/List/index.tsx
new file mode 100644
index 00000000..5ebfcde9
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/List/index.tsx
@@ -0,0 +1,13 @@
+/*
+ * @Author: 赵伟
+ * @Date: 2024-04-16 13:58:08
+ * @Description: 超参数自动寻优
+ */
+
+import ExperimentList, { ExperimentListType } from '@/pages/AutoML/components/ExperimentList';
+
+function HyperParameter() {
+ return ;
+}
+
+export default HyperParameter;
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx
new file mode 100644
index 00000000..9d06fd10
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx
@@ -0,0 +1,58 @@
+import SubAreaTitle from '@/components/SubAreaTitle';
+import { Col, Form, Input, Row } from 'antd';
+
+function BasicConfig() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default BasicConfig;
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx
new file mode 100644
index 00000000..01bc4001
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx
@@ -0,0 +1,611 @@
+import CodeSelect from '@/components/CodeSelect';
+import KFIcon from '@/components/KFIcon';
+import ParameterSelect from '@/components/ParameterSelect';
+import ResourceSelect, {
+ ResourceSelectorType,
+ requiredValidator,
+} from '@/components/ResourceSelect';
+import SubAreaTitle from '@/components/SubAreaTitle';
+import { hyperParameterOptimizedModeOptions } from '@/enums';
+import { isEmpty } from '@/utils';
+import { modalConfirm, removeFormListItem } from '@/utils/ui';
+import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons';
+import {
+ Button,
+ Col,
+ Flex,
+ Form,
+ Input,
+ InputNumber,
+ Radio,
+ Row,
+ Select,
+ Tooltip,
+ Typography,
+} from 'antd';
+import { isEqual } from 'lodash';
+import PopParameterRange from './PopParameterRange';
+import styles from './index.less';
+import {
+ axParameterOptions,
+ parameterOptions,
+ schedulerAlgorithms,
+ searchAlgorithms,
+ type FormParameter,
+} from './utils';
+
+const parameterTooltip = `uniform(low, high)
+ 在 low 和 high 之间均匀采样浮点数
+
+ quniform(low, high, q)
+ 在 low 和 high 之间均匀采样浮点数,四舍五入到 q 的倍数
+
+ loguniform(low, high)
+ 在 low 和 high 之间均匀采样浮点数,对数空间采样
+
+ qloguniform(low, high, q)
+ 在 low 和 high 之间均匀采样浮点数,对数空间采样并四舍五入到 q 的倍数
+
+ randn(m, s)
+ 在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样
+
+ qrandn(m, s, q)
+ 在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样,四舍五入到 q 的倍数
+
+ randint(low, high)
+ 在 low(包括)到 high(不包括)之间均匀采样整数
+
+ qrandint(low, high, q)
+ 在 low(包括)到 high(不包括)之间均匀采样整数,四舍五入到 q 的倍数(包括 high)
+
+ lograndint(low, high)
+ 在 low(包括)到 high(不包括)之间对数空间上均匀采样整数
+
+ qlograndint(low, high, q)
+ 在 low(包括)到 high(不包括)之间对数空间上均匀采样整数,并四舍五入到 q 的倍数
+
+ choice
+ 从指定的选项中采样一个选项
+
+ grid
+ 对选项进行网格搜索,每个值都将被采样
+`;
+
+const axParameterTooltip = `fixed
+ 固定取值
+
+ range(low, high)
+ 在 low 和 high 范围内采样取值
+
+ choice
+ 从指定的选项中采样一个选项
+ `;
+
+function ExecuteConfig() {
+ const form = Form.useFormInstance();
+ const searchAlgorithm = Form.useWatch('search_alg', form);
+ const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions;
+ const paramsTypeTooltip = searchAlgorithm === 'Ax' ? axParameterTooltip : parameterTooltip;
+
+ const handleSearchAlgorithmChange = (value: string) => {
+ if (
+ (value === 'Ax' && searchAlgorithm !== 'Ax') ||
+ (value !== 'Ax' && searchAlgorithm === 'Ax')
+ ) {
+ form.setFieldValue('parameters', [{ name: '' }]);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {({ getFieldValue }) => {
+ const schedulerAlgorithm = getFieldValue('scheduler');
+ if (schedulerAlgorithm === 'ASHA' || schedulerAlgorithm === 'HyperBand') {
+ return (
+
+
+
+
+
+
+
+ );
+ } else if (schedulerAlgorithm === 'MedianStopping') {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ return null;
+ }}
+
+
+
+ {(fields, { add, remove }) => (
+ <>
+
+
+
+
+
+
+
+ 参数名称
+
+ 参数类型
+
+
+
+
+ 取值范围
+ 操作
+
+
+ {fields.map(({ key, name, ...restField }, index) => (
+
+ {
+ if (!value) {
+ return Promise.reject(new Error('请输入参数名称'));
+ }
+ // 判断不能重名
+ const list = form
+ .getFieldValue('parameters')
+ .filter(
+ (item: FormParameter | undefined) =>
+ item !== undefined && item !== null,
+ );
+
+ const names = list.map((item: FormParameter) => item.name);
+ if (new Set(names).size !== names.length) {
+ return Promise.reject(new Error('名称不能重复'));
+ }
+ return Promise.resolve();
+ },
+ },
+ ]}
+ >
+
+
+
+
+
+ {({ getFieldValue }) => {
+ const type = getFieldValue(['parameters', name, 'type']);
+ return (
+
+
+
+ );
+ }}
+
+
+
+
}
+ onClick={() => {
+ removeFormListItem(
+ form,
+ 'parameters',
+ name,
+ remove,
+ ['name', 'type'],
+ '删除后,该参数将不可恢复',
+ );
+ }}
+ >
+ {index === fields.length - 1 && (
+
+ )}
+
+
+ ))}
+ {fields.length === 0 && (
+
+
+
+ )}
+
+ >
+ )}
+
+
+
+ !isEqual(prevValues.parameters, curValues.parameters)
+ }
+ >
+ {({ getFieldValue }) => {
+ const parameters = getFieldValue('parameters').filter(
+ (item: FormParameter | undefined) => item !== undefined && item !== null && item.name,
+ );
+ if (parameters.length === 0) {
+ return null;
+ }
+
+ return (
+ {
+ const parameters = form
+ .getFieldValue('parameters')
+ .filter(
+ (item: FormParameter | undefined) => item !== undefined && item !== null,
+ );
+ for (const item of parameters) {
+ const name = item?.name;
+ const arr = runParameters.filter((item?: Record) =>
+ isEmpty(item?.[name]),
+ );
+ if (arr.length > 0 && arr.length < runParameters.length) {
+ return Promise.reject(
+ new Error(`手动运行超参数 "${name}" 必须全部填写或者都不填写`),
+ );
+ }
+ }
+
+ return Promise.resolve();
+ },
+ },
+ ]}
+ >
+ {(fields, { add, remove }, { errors }) => (
+ <>
+
+
+
+
+
+
+ {fields.map(({ key, name, ...restField }, index) => (
+
+
+ {parameters.map((item: FormParameter) => (
+
+ {item.name}
+
+ }
+ {...restField}
+ labelCol={{ flex: '140px' }}
+ name={[name, item.name]}
+ preserve={false}
+ >
+ form.validateFields(['points_to_evaluate'])}
+ />
+
+ ))}
+
+
+
}
+ onClick={() => {
+ modalConfirm({
+ title: '删除后,该运行参数将不可恢复',
+ content: '是否确认删除?',
+ onOk: () => {
+ remove(name);
+ },
+ });
+ }}
+ >
+ {index === fields.length - 1 && (
+
+ )}
+
+
+ ))}
+ {fields.length === 0 && (
+
+
+
+ )}
+
+
+ >
+ )}
+
+ );
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default ExecuteConfig;
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less
new file mode 100644
index 00000000..25edd4bc
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less
@@ -0,0 +1,47 @@
+.parameter-range {
+ width: 360px;
+ &__type {
+ margin-bottom: 10px;
+ color: @text-color-secondary;
+ font-size: @font-size-input;
+ &::before {
+ display: inline-block;
+ color: @error-color;
+ font-size: 14px;
+ font-family: SimSun, sans-serif;
+ line-height: 1;
+ content: '*';
+ margin-inline-end: 4px;
+ }
+ }
+ &__desc {
+ margin-bottom: 15px;
+ padding: 4px 8px;
+ color: @text-color-tertiary;
+ font-size: 13px;
+ background: rgba(62, 96, 163, 0.05);
+ border-radius: 6px;
+ }
+ &__form {
+ width: 100%;
+ &__list {
+ width: 100%;
+ max-height: 300px;
+ overflow-x: visible;
+ overflow-y: auto;
+ }
+ &__space {
+ flex: none;
+ width: 22px;
+ color: @text-color-tertiary;
+ font-size: @font-size-input;
+ line-height: 32px;
+ text-align: center;
+ }
+ &__button {
+ width: 100%;
+ margin-bottom: 0;
+ text-align: center;
+ }
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx
new file mode 100644
index 00000000..d33fcf13
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx
@@ -0,0 +1,152 @@
+import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
+import { Button, Flex, Form, Input, InputNumber } from 'antd';
+import React from 'react';
+import { ParameterType, getFormOptions, parameterTooltip } from '../utils';
+import styles from './index.less';
+
+type ParameterRangeProps = {
+ type: ParameterType;
+ value?: any[];
+ onCancel?: () => void;
+ onConfirm?: (value: any[]) => void;
+};
+
+function ParameterRange({ type, value, onConfirm }: ParameterRangeProps) {
+ const [form] = Form.useForm();
+ const isList = type === ParameterType.Choice || type === ParameterType.Grid;
+ const formOptions = getFormOptions(type, value);
+
+ const initialValues = isList
+ ? { list: value && value.length > 0 ? value.map((item) => ({ value: item })) : [{ value: '' }] }
+ : formOptions.reduce((prev, item) => {
+ prev[item.name] = item.value;
+ return prev;
+ }, {} as Record);
+
+ const handleFinish = (values: any) => {
+ if (type === ParameterType.Choice || type === ParameterType.Grid) {
+ const array = values.list.map((item: any) => item.value);
+ onConfirm?.(array);
+ } else {
+ const numbers = Object.values(values).map((item: any) => Number(item));
+ onConfirm?.(numbers);
+ }
+ };
+
+ return (
+
+
{type}
+
{parameterTooltip[type]}
+
+
+
+
+
+ );
+}
+
+export default ParameterRange;
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less
new file mode 100644
index 00000000..92080c3e
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less
@@ -0,0 +1,83 @@
+.parameter-range {
+ border-radius: 18px;
+ box-shadow: 0px 3px 10px rgba(22, 100, 255, 0.15);
+ :global {
+ .ant-popover-content {
+ .ant-popover-inner {
+ width: 400px;
+ padding: 20px 20px 12px;
+ background-image: url(@/assets/img/popover-bg.png);
+ background-repeat: no-repeat;
+ background-position: top left;
+ background-size: 100% auto;
+ }
+ .ant-popconfirm-description {
+ margin-top: 20px;
+ }
+
+ .ant-popconfirm-buttons {
+ display: none;
+ }
+ }
+ }
+
+ &__input {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ min-height: 46px;
+ padding: 10px 11px;
+ color: @text-color;
+ font-size: @font-size-input-lg;
+ line-height: 1.5;
+ background-color: white;
+ border: 1px solid #d9d9d9;
+ border-radius: 8px;
+ cursor: pointer;
+
+ &__text {
+ flex: 1;
+ margin-right: 10px;
+ }
+
+ &__icon {
+ flex: none;
+ }
+
+ &:hover {
+ border-color: #4086ff;
+ }
+
+ &:hover &__icon {
+ color: #4086ff;
+ }
+
+ &&--disabled {
+ background-color: rgba(0, 0, 0, 0.04);
+ border-color: rgba(0, 0, 0, 0.04) !important;
+ cursor: not-allowed;
+ }
+
+ &&--empty {
+ color: @text-placeholder-color;
+ }
+
+ &&--disabled &__icon {
+ color: #aaaaaa !important;
+ }
+ }
+}
+
+.parameter-range-title {
+ color: @text-color;
+ font-weight: 500;
+ font-size: @font-size-content;
+}
+
+.parameter-range-title-icon {
+ color: @text-color-secondary;
+
+ &:hover {
+ color: @text-color;
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx
new file mode 100644
index 00000000..b5db36b9
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx
@@ -0,0 +1,105 @@
+import KFIcon from '@/components/KFIcon';
+import { isEmpty } from '@/utils';
+import { Flex, Popconfirm, Typography } from 'antd';
+import classNames from 'classnames';
+import { useEffect, useRef, useState } from 'react';
+import ParameterRange from '../ParameterRange';
+import { ParameterType } from '../utils';
+import styles from './index.less';
+
+type ParameterRangeProps = {
+ type: ParameterType;
+ value?: any[];
+ onChange?: (value: any[]) => void;
+};
+
+function PopParameterRange({ type, value, onChange }: ParameterRangeProps) {
+ const [open, setOpen] = useState(false);
+ const popconfirmRef = useRef(null);
+ const disabled = !type;
+ const jsonText = JSON.stringify(value);
+
+ const handleClickOutside = (event: MouseEvent) => {
+ // 判断点击是否在 Popconfirm 内
+ const popconfirmNode = document.getElementById('pop-parameter');
+ if (popconfirmNode && !popconfirmNode.contains(event.target as Node)) {
+ setOpen(false);
+ }
+ };
+
+ useEffect(() => {
+ if (open) {
+ document.addEventListener('mousedown', handleClickOutside);
+ } else {
+ document.removeEventListener('mousedown', handleClickOutside);
+ }
+ // 清理事件监听器
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [open]);
+
+ const handleClick = () => {
+ if (!disabled) {
+ setOpen(true);
+ }
+ };
+
+ const handleCancel = () => {
+ setOpen(false);
+ };
+
+ const handleConfirm = (value: number[]) => {
+ onChange?.(value);
+ setOpen(false);
+ };
+
+ return (
+
+
}
+ disabled={disabled}
+ description={
+
+ }
+ overlayClassName={styles['parameter-range']}
+ icon={null}
+ open={open}
+ destroyTooltipOnHide
+ >
+
+
+ {jsonText ?? '请选择'}
+
+
+
+
+
+ );
+}
+
+function PopconfirmTitle({ title, onClose }: { title: string; onClose: () => void }) {
+ return (
+
+ {title}
+
+
+ );
+}
+
+export default PopParameterRange;
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less
new file mode 100644
index 00000000..bd264d3c
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less
@@ -0,0 +1,146 @@
+.metrics-weight {
+ margin-bottom: 20px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.add-weight {
+ margin-bottom: 0 !important;
+
+ // 增加样式权重
+ & &__button {
+ width: calc(100% - 126px);
+ border-color: .addAlpha(@primary-color, 0.5) [];
+ box-shadow: none !important;
+ &:hover {
+ border-style: solid;
+ }
+ }
+}
+
+.hyper-parameter {
+ width: 83.33%;
+ margin-bottom: 20px;
+ border: 1px solid rgba(234, 234, 234, 0.8);
+ border-radius: 4px;
+ &__header {
+ height: 50px;
+ padding-left: 8px;
+ color: @text-color;
+ font-size: @font-size;
+ background: #f8f8f9;
+ border-radius: 4px 4px 0px 0px;
+
+ &__name,
+ &__type,
+ &__space {
+ flex: 1;
+ min-width: 0;
+ margin-right: 15px;
+
+ &::before {
+ display: inline-block;
+ color: @error-color;
+ font-size: 14px;
+ font-family: SimSun, sans-serif;
+ line-height: 1;
+ content: '*';
+ margin-inline-end: 4px;
+ }
+
+ :global {
+ .anticon-question-circle {
+ vertical-align: middle;
+ cursor: help;
+ }
+ }
+ }
+
+ &__tooltip {
+ max-width: 600px;
+ :global {
+ .ant-tooltip-inner {
+ max-height: 400px;
+ overflow-y: auto;
+ white-space: pre-line;
+
+ &::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.5);
+ }
+ }
+ }
+ }
+
+ &__operation {
+ flex: none;
+ width: 100px;
+ }
+ }
+ &__body {
+ padding: 8px;
+ border-bottom: 1px solid rgba(234, 234, 234, 0.8);
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &__name,
+ &__type,
+ &__space {
+ flex: 1;
+ min-width: 0;
+ margin-right: 15px;
+ margin-bottom: 0 !important;
+ }
+
+ &__operation {
+ display: flex;
+ flex: none;
+ align-items: center;
+ width: 100px;
+ height: 46px;
+ }
+ }
+
+ &__add {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 15px 0;
+ }
+}
+
+.run-parameter {
+ width: calc(41.66% + 126px);
+ margin-bottom: 20px;
+
+ &__body {
+ flex: 1;
+ margin-right: 10px;
+ padding: 20px 20px 0;
+ border: 1px dashed #e0e0e0;
+ border-radius: 8px;
+
+ :global {
+ .ant-form-item-label {
+ label {
+ width: calc(100% - 10px);
+ }
+ }
+ }
+ }
+
+ &__operation {
+ display: flex;
+ flex: none;
+ align-items: center;
+ width: 100px;
+ }
+
+ &__error {
+ margin-top: -20px;
+ color: @error-color;
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts
new file mode 100644
index 00000000..558637e9
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts
@@ -0,0 +1,228 @@
+export enum ParameterType {
+ Uniform = 'uniform',
+ QUniform = 'quniform',
+ LogUniform = 'loguniform',
+ QLogUniform = 'qloguniform',
+ Randn = 'randn',
+ QRandn = 'qrandn',
+ RandInt = 'randint',
+ QRandInt = 'qrandint',
+ LogRandInt = 'lograndint',
+ QLogRandInt = 'qlograndint',
+ Choice = 'choice',
+ Grid = 'grid',
+ Range = 'range',
+ Fixed = 'fixed',
+}
+
+export const parameterOptions = [
+ 'uniform',
+ 'quniform',
+ 'loguniform',
+ 'qloguniform',
+ 'randn',
+ 'qrandn',
+ 'randint',
+ 'qrandint',
+ 'lograndint',
+ 'qlograndint',
+ 'choice',
+ 'grid',
+].map((name) => ({
+ label: name,
+ value: name,
+}));
+
+export const axParameterOptions = ['fixed', 'range', 'choice'].map((name) => ({
+ label: name,
+ value: name,
+}));
+
+export const parameterTooltip: Record = {
+ [ParameterType.Uniform]: '在 low 和 high 之间均匀采样浮点数',
+ [ParameterType.QUniform]: '在 low 和 high 之间均匀采样浮点数,四舍五入到 q 的倍数',
+ [ParameterType.LogUniform]: '在 low 和 high 之间均匀采样浮点数,对数空间采样',
+ [ParameterType.QLogUniform]:
+ '在 low 和 high 之间均匀采样浮点数,对数空间采样并四舍五入到 q 的倍数',
+ [ParameterType.Randn]: '在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样',
+ [ParameterType.QRandn]:
+ '在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样,四舍五入到 q 的倍数',
+ [ParameterType.RandInt]: '在 low(包括)到 high(不包括)之间均匀采样整数',
+ [ParameterType.QRandInt]:
+ '在 low(包括)到 high(不包括)之间均匀采样整数,四舍五入到 q 的倍数(包括 high)',
+ [ParameterType.LogRandInt]: '在 low(包括)到 high(不包括)之间对数空间上均匀采样整数',
+ [ParameterType.QLogRandInt]:
+ '在 low(包括)到 high(不包括)之间对数空间上均匀采样整数,并四舍五入到 q 的倍数',
+ [ParameterType.Choice]: '从指定的选项中采样一个选项',
+ [ParameterType.Grid]: '对选项进行网格搜索,每个值都将被采样',
+ [ParameterType.Range]: '在 low 和 high 范围内采样取值',
+ [ParameterType.Fixed]: '固定取值',
+};
+
+export type ParameterData = {
+ label: string;
+ name: string;
+ value?: number;
+};
+
+// 参数表单数据
+export type FormParameter = {
+ name: string; // 参数名称
+ type: ParameterType; // 参数类型
+ range: any; // 参数值
+ [key: string]: any;
+};
+
+export const getFormOptions = (type?: ParameterType, value?: number[]): ParameterData[] => {
+ const numbers =
+ value?.map((item) => {
+ const num = Number(item);
+ if (isNaN(num)) {
+ return undefined;
+ }
+ return num;
+ }) ?? [];
+ switch (type) {
+ case ParameterType.Uniform:
+ case ParameterType.LogUniform:
+ case ParameterType.RandInt:
+ case ParameterType.LogRandInt:
+ case ParameterType.Range:
+ return [
+ {
+ name: 'low',
+ label: '最小值',
+ value: numbers?.[0],
+ },
+ {
+ name: 'high',
+ label: '最大值',
+ value: numbers?.[1],
+ },
+ ];
+ case ParameterType.QUniform:
+ case ParameterType.QLogUniform:
+ case ParameterType.QRandInt:
+ case ParameterType.QLogRandInt:
+ return [
+ {
+ name: 'low',
+ label: '最小值',
+ value: numbers?.[0],
+ },
+ {
+ name: 'high',
+ label: '最大值',
+ value: numbers?.[1],
+ },
+ {
+ name: 'q',
+ label: '间隔',
+ value: numbers?.[2],
+ },
+ ];
+ case ParameterType.Randn:
+ return [
+ {
+ name: 'm',
+ label: '均值',
+ value: numbers?.[0],
+ },
+ {
+ name: 's',
+ label: '方差',
+ value: numbers?.[1],
+ },
+ ];
+ case ParameterType.QRandn:
+ return [
+ {
+ name: 'm',
+ label: '均值',
+ value: numbers?.[0],
+ },
+ {
+ name: 's',
+ label: '方差',
+ value: numbers?.[1],
+ },
+ {
+ name: 'q',
+ label: '间隔',
+ value: numbers?.[2],
+ },
+ ];
+ case ParameterType.Fixed:
+ return [
+ {
+ name: 'value',
+ label: '值',
+ value: numbers?.[0],
+ },
+ ];
+ default:
+ return [];
+ }
+};
+
+export const getReqParamName = (type: ParameterType) => {
+ if (type === ParameterType.Fixed) {
+ return 'value';
+ } else if (type === ParameterType.Choice || type === ParameterType.Grid) {
+ return 'values';
+ } else {
+ return 'bounds';
+ }
+};
+
+// 搜索算法
+export const searchAlgorithms = [
+ {
+ label: 'HyperOpt(分布式异步超参数优化)',
+ value: 'HyperOpt',
+ },
+ {
+ label: 'HEBO(异方差进化贝叶斯优化)',
+ value: 'HEBO',
+ },
+ {
+ label: 'BayesOpt(贝叶斯优化)',
+ value: 'BayesOpt',
+ },
+ {
+ label: 'Optuna',
+ value: 'Optuna',
+ },
+ {
+ label: 'ZOOpt',
+ value: 'ZOOpt',
+ },
+ {
+ label: 'Ax',
+ value: 'Ax',
+ },
+];
+
+// 调度算法
+export const schedulerAlgorithms = [
+ {
+ label: 'ASHA(异步连续减半)',
+ value: 'ASHA',
+ },
+ {
+ label: 'HyperBand(HyperBand 早停算法)',
+ value: 'HyperBand',
+ },
+ {
+ label: 'MedianStopping(中值停止规则)',
+ value: 'MedianStopping',
+ },
+ {
+ label: 'PopulationBased(基于种群训练)',
+ value: 'PopulationBased',
+ },
+ {
+ label: 'PB2(Population Based Bandits)',
+ value: 'PB2',
+ },
+];
diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less
new file mode 100644
index 00000000..761c2c9b
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less
@@ -0,0 +1,58 @@
+.experiment-history {
+ height: calc(100% - 10px);
+ margin-top: 10px;
+ &__content {
+ height: 100%;
+ padding: 20px @content-padding;
+ background-color: white;
+ border-radius: 10px;
+
+ &__table {
+ height: calc(100% - 52px);
+ margin-top: 20px;
+ }
+
+ :global {
+ .ant-table-container {
+ border: none !important;
+ }
+ .ant-table-thead {
+ .ant-table-cell {
+ background-color: rgb(247, 247, 247);
+ border-color: @border-color !important;
+ }
+ }
+ .ant-table-tbody {
+ .ant-table-cell {
+ border-right: none !important;
+ border-left: none !important;
+ }
+ }
+ .ant-table-tbody-virtual::after {
+ border-bottom: none !important;
+ }
+ }
+ }
+}
+
+.cell-index {
+ position: relative;
+ width: 100%;
+ white-space: nowrap;
+
+ &__best-tag {
+ margin-left: 8px;
+ padding: 1px 10px;
+ color: @success-color;
+ font-weight: normal;
+ font-size: 13px;
+ white-space: nowrap;
+ background-color: .addAlpha(@success-color, 0.1) [];
+ border-radius: 2px;
+ }
+}
+
+.table-best-row {
+ color: @success-color;
+ font-weight: bold;
+}
diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx
new file mode 100644
index 00000000..c9c9dce5
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx
@@ -0,0 +1,202 @@
+import TableColTitle from '@/components/TableColTitle';
+import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell';
+import { HyperParameterTrial } from '@/pages/HyperParameter/types';
+import { getExpMetricsReq } from '@/services/hyperParameter';
+import { to } from '@/utils/promise';
+import tableCellRender, { TableCellValueType } from '@/utils/table';
+import { App, Button, Table, type TableProps } from 'antd';
+import classNames from 'classnames';
+import { useEffect, useState } from 'react';
+import TrialFileTree from '../TrialFileTree';
+import styles from './index.less';
+
+type ExperimentHistoryProps = {
+ trialList?: HyperParameterTrial[];
+};
+
+function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
+ const [expandedRowKeys, setExpandedRowKeys] = useState([]);
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const { message } = App.useApp();
+ const [tableData, setTableData] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ // 防止 Tabs 卡顿
+ useEffect(() => {
+ setLoading(true);
+ setTimeout(() => {
+ setTableData(trialList);
+ setLoading(false);
+ }, 500);
+ }, [trialList]);
+
+ // 计算 column
+ const first: HyperParameterTrial | undefined = trialList ? trialList[0] : undefined;
+ const config: Record = first?.config ?? {};
+ const metricAnalysis: Record = first?.metric_analysis ?? {};
+ const paramsNames = Object.keys(config);
+ const metricNames = Object.keys(metricAnalysis);
+
+ const trialColumns: TableProps['columns'] = [
+ {
+ title: '序号',
+ dataIndex: 'index',
+ key: 'index',
+ width: 100,
+ fixed: 'left',
+ render: (_text, record, index: number) => {
+ return (
+
+ {index + 1}
+ {record.is_best && 最佳}
+
+ );
+ },
+ },
+ {
+ title: '基本信息',
+ align: 'center',
+ children: [
+ {
+ title: '运行次数',
+ dataIndex: 'training_iteration',
+ key: 'training_iteration',
+ width: 120,
+ fixed: 'left',
+ render: tableCellRender(false),
+ },
+ {
+ title: '平均时长(秒)',
+ dataIndex: 'time_avg',
+ key: 'time_avg',
+ width: 150,
+ fixed: 'left',
+ render: tableCellRender(false, TableCellValueType.Custom, {
+ format: (value = 0) => Number(value).toFixed(2),
+ }),
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 120,
+ fixed: 'left',
+ render: TrialStatusCell,
+ },
+ ],
+ },
+ ];
+
+ if (paramsNames.length) {
+ trialColumns.push({
+ title: '运行参数',
+ dataIndex: 'config',
+ key: 'config',
+ align: 'center',
+ children: paramsNames.map((name) => ({
+ title: ,
+ dataIndex: ['config', name],
+ key: name,
+ width: 120,
+ align: 'center',
+ render: tableCellRender(true),
+ })),
+ });
+ }
+
+ if (metricNames.length) {
+ trialColumns.push({
+ title: `指标分析(${first?.metric ?? ''})`,
+ dataIndex: 'metrics',
+ key: 'metrics',
+ align: 'center',
+ children: metricNames.map((name) => ({
+ title: ,
+ dataIndex: ['metric_analysis', name],
+ key: name,
+ width: 120,
+ align: 'center',
+ render: tableCellRender(true),
+ })),
+ });
+ }
+
+ // 自定义展开视图
+ const expandedRowRender = (record: HyperParameterTrial) => {
+ return ;
+ };
+
+ // 展开实例
+ const handleExpandChange = (expanded: boolean, record: HyperParameterTrial) => {
+ if (expanded) {
+ setExpandedRowKeys([record.trial_id]);
+ } else {
+ setExpandedRowKeys([]);
+ }
+ };
+
+ // 选择行
+ const rowSelection: TableProps['rowSelection'] = {
+ type: 'checkbox',
+ columnWidth: 48,
+ fixed: 'left',
+ selectedRowKeys,
+ onChange: (selectedRowKeys: React.Key[]) => {
+ setSelectedRowKeys(selectedRowKeys);
+ },
+ };
+
+ // 对比
+ const handleComparisonClick = () => {
+ if (selectedRowKeys.length < 1) {
+ message.error('请至少选择一项');
+ return;
+ }
+ getExpMetrics();
+ };
+
+ // 获取对比 url
+ const getExpMetrics = async () => {
+ const [res] = await to(getExpMetricsReq(selectedRowKeys));
+ if (res && res.data) {
+ const url = res.data;
+ window.open(url, '_blank');
+ }
+ };
+
+ return (
+
+
+
+
+
(record.is_best ? styles['table-best-row'] : '')}
+ dataSource={tableData}
+ columns={trialColumns}
+ pagination={false}
+ bordered={true}
+ scroll={{ y: 'calc(100% - 110px)', x: '100%' }}
+ rowKey="trial_id"
+ expandable={{
+ expandedRowRender: expandedRowRender,
+ onExpand: handleExpandChange,
+ expandedRowKeys: expandedRowKeys,
+ rowExpandable: (record: HyperParameterTrial) => !!record.file,
+ }}
+ rowSelection={rowSelection}
+ />
+
+
+
+ );
+}
+
+export default ExperimentHistory;
diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less
new file mode 100644
index 00000000..6eb6f074
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less
@@ -0,0 +1,16 @@
+.experiment-log {
+ height: 100%;
+ &__tabs {
+ height: 100%;
+ :global {
+ .ant-tabs-nav-list {
+ padding-left: 0 !important;
+ background: none !important;
+ }
+ }
+
+ &__log {
+ height: 100%;
+ }
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx
new file mode 100644
index 00000000..d7639094
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx
@@ -0,0 +1,108 @@
+import { ExperimentStatus } from '@/enums';
+import LogList from '@/pages/Experiment/components/LogList';
+import { HyperParameterInstanceData } from '@/pages/HyperParameter/types';
+import { NodeStatus } from '@/types';
+import { Tabs } from 'antd';
+import styles from './index.less';
+
+const NodePrefix = 'auto-hpo';
+
+type ExperimentLogProps = {
+ instanceInfo: HyperParameterInstanceData;
+ nodes: Record;
+};
+
+function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
+ let hpoNodeStatus: NodeStatus | undefined;
+ let frameworkCloneNodeStatus: NodeStatus | undefined;
+ let trainCloneNodeStatus: NodeStatus | undefined;
+
+ Object.keys(nodes)
+ .sort((key1, key2) => {
+ const node1 = nodes[key1];
+ const node2 = nodes[key2];
+ return new Date(node1.startedAt).getTime() - new Date(node2.startedAt).getTime();
+ })
+ .forEach((key) => {
+ const node = nodes[key];
+ if (node.displayName.startsWith(NodePrefix)) {
+ hpoNodeStatus = node;
+ } else if (node.displayName.startsWith('git-clone') && !frameworkCloneNodeStatus) {
+ frameworkCloneNodeStatus = node;
+ } else if (
+ node.displayName.startsWith('git-clone') &&
+ frameworkCloneNodeStatus &&
+ node.displayName !== frameworkCloneNodeStatus?.displayName
+ ) {
+ trainCloneNodeStatus = node;
+ }
+ });
+
+ const tabItems = [
+ // {
+ // key: 'git-clone-framework',
+ // label: '框架代码日志',
+ // // icon: ,
+ // children: (
+ //
+ // {frameworkCloneNodeStatus && (
+ //
+ // )}
+ //
+ // ),
+ // },
+ {
+ key: 'git-clone-train',
+ label: '系统日志',
+ // icon: ,
+ children: (
+
+ {trainCloneNodeStatus && (
+
+ )}
+
+ ),
+ },
+ {
+ key: 'auto-hpo',
+ label: '超参寻优日志',
+ // icon: ,
+ children: (
+
+ {hpoNodeStatus && (
+
+ )}
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ );
+}
+
+export default ExperimentLog;
diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less
new file mode 100644
index 00000000..239a3abf
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less
@@ -0,0 +1,17 @@
+.experiment-result {
+ height: calc(100% - 10px);
+ margin-top: 10px;
+ padding: 20px @content-padding;
+ overflow-y: auto;
+ background-color: white;
+ border-radius: 10px;
+
+ &__table {
+ height: 400px;
+ }
+
+ &__text {
+ font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
+ white-space: pre;
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx
new file mode 100644
index 00000000..4a7d3dcc
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx
@@ -0,0 +1,37 @@
+import InfoGroup from '@/components/InfoGroup';
+import { getFileReq } from '@/services/file';
+import { to } from '@/utils/promise';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+type ExperimentResultProps = {
+ fileUrl?: string;
+};
+
+function ExperimentResult({ fileUrl }: ExperimentResultProps) {
+ const [result, setResult] = useState('');
+
+ useEffect(() => {
+ // 获取实验运行历史记录
+ const getResultFile = async () => {
+ const [res] = await to(getFileReq(fileUrl));
+ if (res) {
+ setResult(res as any as string);
+ }
+ };
+
+ if (fileUrl) {
+ getResultFile();
+ }
+ }, [fileUrl]);
+
+ return (
+
+ );
+}
+
+export default ExperimentResult;
diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less
new file mode 100644
index 00000000..f365aa66
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less
@@ -0,0 +1,13 @@
+.hyper-parameter-basic {
+ height: 100%;
+ padding: 20px @content-padding;
+ overflow-y: auto;
+ background-color: white;
+ border-radius: 10px;
+
+ :global {
+ .kf-basic-info__item__value__text {
+ white-space: pre;
+ }
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx
new file mode 100644
index 00000000..f4b9852b
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx
@@ -0,0 +1,219 @@
+import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
+import RunDuration from '@/components/RunDuration';
+import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums';
+import { useComputingResource } from '@/hooks/useComputingResource';
+import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic';
+import { experimentStatusInfo } from '@/pages/Experiment/status';
+import {
+ schedulerAlgorithms,
+ searchAlgorithms,
+} from '@/pages/HyperParameter/components/CreateForm/utils';
+import { HyperParameterData } from '@/pages/HyperParameter/types';
+import { type NodeStatus } from '@/types';
+import {
+ formatCodeConfig,
+ formatDataset,
+ formatDate,
+ formatEnum,
+ formatMirror,
+ formatModel,
+} from '@/utils/format';
+import { Flex } from 'antd';
+import classNames from 'classnames';
+import { useMemo } from 'react';
+import ParameterInfo from '../ParameterInfo';
+import styles from './index.less';
+
+// 格式化优化方向
+const formatOptimizeMode = (value: string) => {
+ return value === hyperParameterOptimizedMode.Max ? '越大越好' : '越小越好';
+};
+
+type HyperParameterBasicProps = {
+ info?: HyperParameterData;
+ className?: string;
+ isInstance?: boolean;
+ runStatus?: NodeStatus;
+ instanceStatus?: ExperimentStatus;
+};
+
+function HyperParameterBasic({
+ info,
+ className,
+ runStatus,
+ instanceStatus,
+ isInstance = false,
+}: HyperParameterBasicProps) {
+ const getResourceDescription = useComputingResource()[1];
+
+ const basicDatas: BasicInfoData[] = useMemo(() => {
+ if (!info) {
+ return [];
+ }
+
+ return [
+ {
+ label: '实验名称',
+ value: info.name,
+ },
+ {
+ label: '实验描述',
+ value: info.description,
+ },
+ {
+ label: '创建人',
+ value: info.create_by,
+ },
+ {
+ label: '创建时间',
+ value: info.create_time,
+ format: formatDate,
+ },
+ {
+ label: '更新时间',
+ value: info.update_time,
+ format: formatDate,
+ },
+ ];
+ }, [info]);
+
+ const configDatas: BasicInfoData[] = useMemo(() => {
+ if (!info) {
+ return [];
+ }
+ return [
+ {
+ label: '代码',
+ value: info.code_config,
+ format: formatCodeConfig,
+ },
+ {
+ label: '主函数代码文件',
+ value: info.main_py,
+ },
+ {
+ label: '镜像',
+ value: info.image,
+ format: formatMirror,
+ },
+ {
+ label: '数据集',
+ value: info.dataset,
+ format: formatDataset,
+ },
+ {
+ label: '模型',
+ value: info.model,
+ format: formatModel,
+ },
+ {
+ label: '总试验次数',
+ value: info.num_samples,
+ },
+ {
+ label: '搜索算法',
+ value: info.search_alg,
+ format: formatEnum(searchAlgorithms),
+ },
+ {
+ label: '调度算法',
+ value: info.scheduler,
+ format: formatEnum(schedulerAlgorithms),
+ },
+ {
+ label: '单次试验最大时间',
+ value: info.max_t,
+ },
+ {
+ label: '最小试验数',
+ value: info.min_samples_required,
+ },
+ {
+ label: '指标优化方向',
+ value: info.mode,
+ format: formatOptimizeMode,
+ },
+ {
+ label: '指标',
+ value: info.metric,
+ },
+ {
+ label: '资源规格',
+ value: info.computing_resource_id,
+ format: getResourceDescription,
+ },
+ ];
+ }, [info, getResourceDescription]);
+
+ const instanceDatas = useMemo(() => {
+ if (!info || !runStatus) {
+ return [];
+ }
+
+ return [
+ {
+ label: '启动时间',
+ value: formatDate(info.create_time),
+ ellipsis: true,
+ },
+ {
+ label: '执行时长',
+ value: ,
+ ellipsis: true,
+ },
+ {
+ label: '状态',
+ value: (
+
+
+
+ {experimentStatusInfo[runStatus?.phase]?.label}
+
+
+ ),
+ ellipsis: true,
+ },
+ ];
+ }, [runStatus, info]);
+
+ return (
+
+ {isInstance && runStatus && (
+
+ )}
+ {!isInstance && (
+
+ )}
+
+ {info && }
+
+
+ );
+}
+
+export default HyperParameterBasic;
diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less
new file mode 100644
index 00000000..81d6fd56
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less
@@ -0,0 +1,7 @@
+.parameter-info {
+ &__title {
+ margin: 20px 0;
+ color: @text-color-secondary;
+ font-size: @font-size;
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx
new file mode 100644
index 00000000..337f43f7
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx
@@ -0,0 +1,107 @@
+import TableColTitle from '@/components/TableColTitle';
+import {
+ getReqParamName,
+ type FormParameter,
+} from '@/pages/HyperParameter/components/CreateForm/utils';
+import { HyperParameterData } from '@/pages/HyperParameter/types';
+import tableCellRender, { TableCellValueType } from '@/utils/table';
+import { Table, type TableProps } from 'antd';
+import { useMemo } from 'react';
+import styles from './index.less';
+
+type ParameterInfoProps = {
+ info: HyperParameterData;
+};
+
+function ParameterInfo({ info }: ParameterInfoProps) {
+ const parameters = useMemo(() => {
+ if (!info.parameters) {
+ return [];
+ }
+ return info.parameters.map((item) => {
+ const paramName = getReqParamName(item.type);
+ const range = item[paramName];
+ return {
+ ...item,
+ range,
+ };
+ });
+ }, [info]);
+
+ const runParameters = useMemo(() => {
+ if (!info.points_to_evaluate) {
+ return [];
+ }
+ return info.points_to_evaluate.map((item, index) => ({
+ ...item,
+ id: index, // 作为 key,这个数组不会变化
+ }));
+ }, [info]);
+
+ const columns: TableProps['columns'] = [
+ {
+ title: '参数名称',
+ dataIndex: 'name',
+ key: 'type',
+ width: '40%',
+ render: tableCellRender('auto'),
+ },
+ {
+ title: '参数类型',
+ dataIndex: 'type',
+ key: 'type',
+ width: '20%',
+ render: tableCellRender(false),
+ },
+ {
+ title: '取值范围',
+ dataIndex: 'range',
+ key: 'range',
+ width: '40%',
+ render: tableCellRender(true, TableCellValueType.Custom, {
+ format: (value) => {
+ return JSON.stringify(value);
+ },
+ }),
+ },
+ ];
+
+ const runColumns: TableProps>['columns'] =
+ runParameters.length > 0
+ ? parameters.map(({ name }) => {
+ return {
+ title: ,
+ dataIndex: name,
+ key: name,
+ width: 150,
+ render: tableCellRender(true),
+ };
+ })
+ : [];
+
+ return (
+
+ );
+}
+
+export default ParameterInfo;
diff --git a/react-ui/src/pages/HyperParameter/components/TrialFileTree/index.less b/react-ui/src/pages/HyperParameter/components/TrialFileTree/index.less
new file mode 100644
index 00000000..8d19ea0e
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/TrialFileTree/index.less
@@ -0,0 +1,14 @@
+.trail-file-tree {
+ :global {
+ .ant-tree-node-selected {
+ .trail-file-tree__icon {
+ color: white;
+ }
+ }
+
+ .trail-file-tree__icon {
+ margin-left: 8px;
+ color: @primary-color;
+ }
+ }
+}
diff --git a/react-ui/src/pages/HyperParameter/components/TrialFileTree/index.tsx b/react-ui/src/pages/HyperParameter/components/TrialFileTree/index.tsx
new file mode 100644
index 00000000..d8c23d59
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/TrialFileTree/index.tsx
@@ -0,0 +1,66 @@
+import InfoGroup from '@/components/InfoGroup';
+import KFIcon from '@/components/KFIcon';
+import { type HyperParameterFile } from '@/pages/HyperParameter/types';
+import { downLoadZip } from '@/utils/downloadfile';
+import { Tree, type TreeDataNode } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+
+const { DirectoryTree } = Tree;
+
+export type TrialFileTreeProps = {
+ title: string;
+ file?: HyperParameterFile;
+ classname?: string;
+ defaultExpandAll?: boolean;
+};
+
+function TrialFileTree({ file, title, defaultExpandAll = true, classname }: TrialFileTreeProps) {
+ const filesToTreeData = (
+ files: HyperParameterFile[],
+ parent?: HyperParameterFile,
+ ): TreeDataNode[] =>
+ files.map((file) => {
+ const key = parent ? `${parent.name}/${file.name}` : file.name;
+ return {
+ ...file,
+ key,
+ title: file.name,
+ children: file.children ? filesToTreeData(file.children, file) : undefined,
+ };
+ });
+
+ const treeData: TreeDataNode[] = filesToTreeData(file ? [file] : []);
+ return (
+
+ {
+ const label = record.title + (record.isFile ? `(${record.size})` : '');
+ return (
+ <>
+ {label}
+ {
+ e.stopPropagation();
+ downLoadZip(
+ record.isFile
+ ? `/api/mmp/minioStorage/downloadFile`
+ : `/api/mmp/minioStorage/download`,
+ { path: record.url },
+ );
+ }}
+ />
+ >
+ );
+ }}
+ />
+
+ );
+}
+
+export default TrialFileTree;
diff --git a/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less
new file mode 100644
index 00000000..6bdaf5bc
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less
@@ -0,0 +1,3 @@
+.trial-status-cell {
+ height: 100%;
+}
diff --git a/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx
new file mode 100644
index 00000000..8838e175
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx
@@ -0,0 +1,67 @@
+/*
+ * @Author: 赵伟
+ * @Date: 2024-04-18 18:35:41
+ * @Description: 实验状态
+ */
+
+import { HyperParameterTrailStatus } from '@/enums';
+import { ExperimentStatusInfo } from '@/pages/Experiment/status';
+import themes from '@/styles/theme.less';
+import styles from './index.less';
+
+export const statusInfo: Record = {
+ [HyperParameterTrailStatus.RUNNING]: {
+ label: '运行中',
+ color: themes.primaryColor,
+ icon: '/assets/images/experiment-status/running-icon.png',
+ },
+ [HyperParameterTrailStatus.TERMINATED]: {
+ label: '成功',
+ color: themes.successColor,
+ icon: '/assets/images/experiment-status/success-icon.png',
+ },
+ [HyperParameterTrailStatus.PENDING]: {
+ label: '挂起',
+ color: themes.pendingColor,
+ icon: '/assets/images/experiment-status/pending-icon.png',
+ },
+ [HyperParameterTrailStatus.ERROR]: {
+ label: '失败',
+ color: themes.errorColor,
+ icon: '/assets/images/experiment-status/fail-icon.png',
+ },
+ [HyperParameterTrailStatus.PAUSED]: {
+ label: '暂停',
+ color: themes.abortColor,
+ icon: '/assets/images/experiment-status/omitted-icon.png',
+ },
+ [HyperParameterTrailStatus.RESTORING]: {
+ label: '恢复中',
+ color: themes.textColor,
+ icon: '/assets/images/experiment-status/omitted-icon.png',
+ },
+};
+
+function TrialStatusCell(status?: HyperParameterTrailStatus | null) {
+ if (status === null || status === undefined) {
+ return --;
+ }
+ return (
+
+ {/*

*/}
+
+ {statusInfo[status] ? statusInfo[status].label : status}
+
+
+ );
+}
+
+export default TrialStatusCell;
diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts
new file mode 100644
index 00000000..780e203d
--- /dev/null
+++ b/react-ui/src/pages/HyperParameter/types.ts
@@ -0,0 +1,84 @@
+import { type ParameterInputObject } from '@/components/ResourceSelect';
+import { type NodeStatus } from '@/types';
+import { type FormParameter } from './components/CreateForm/utils';
+
+// 操作类型
+export enum OperationType {
+ Create = 'Create', // 创建
+ Update = 'Update', // 更新
+}
+
+// 表单数据
+export type FormData = {
+ name: string; // 实验名称
+ description: string; // 实验描述
+ code_config: ParameterInputObject; // 代码
+ dataset: ParameterInputObject; // 数据集
+ model: ParameterInputObject; // 模型
+ image: ParameterInputObject; // 镜像
+ main_py: string; // 主函数代码文件
+ metric: string; // 指标
+ mode: string; // 优化方向
+ search_alg?: string; // 搜索算法
+ scheduler?: string; // 调度算法
+ num_samples: number; // 总试验次数
+ max_t: number; // 单次试验最大时间
+ min_samples_required: number; // 计算中位数的最小试验数
+ computing_resource_id: number; // 资源规格
+ parameters: FormParameter[];
+ points_to_evaluate: { [key: string]: any }[];
+};
+
+export type HyperParameterData = {
+ id: number;
+ progress: number;
+ run_state: string;
+ state: number;
+ create_by?: string;
+ create_time?: string;
+ update_by?: string;
+ update_time?: string;
+ status_list: string; // 最近五次运行状态
+} & FormData;
+
+// 实验实例
+export type HyperParameterInstanceData = {
+ id: number;
+ ray_id: number;
+ result_path: string;
+ result_txt: string;
+ state: number;
+ status: string;
+ node_status: string;
+ node_result: string;
+ param: string;
+ source: string | null;
+ argo_ins_name: string;
+ argo_ins_ns: string;
+ create_time: string;
+ update_time: string;
+ finish_time: string;
+ nodeStatus?: NodeStatus; // json之后的节点状态
+ trial_list?: HyperParameterTrial[];
+ file_list?: HyperParameterFile[];
+};
+
+export type HyperParameterTrial = {
+ trial_id: string;
+ training_iteration?: number;
+ time?: number;
+ status?: string;
+ config?: Record;
+ metric_analysis?: Record;
+ metric: string;
+ file: HyperParameterFile;
+ is_best?: boolean;
+};
+
+export type HyperParameterFile = {
+ name: string;
+ size: string;
+ url: string;
+ isFile: boolean;
+ children: HyperParameterFile[];
+};
diff --git a/react-ui/src/pages/Knowledge/index.tsx b/react-ui/src/pages/Knowledge/index.tsx
new file mode 100644
index 00000000..a00e5e6c
--- /dev/null
+++ b/react-ui/src/pages/Knowledge/index.tsx
@@ -0,0 +1,12 @@
+/*
+ * @Author: 赵伟
+ * @Date: 2025-04-21 16:38:59
+ * @Description: 知识图谱
+ */
+
+import IframePage, { IframePageType } from '@/components/IFramePage';
+
+function KnowledgePage() {
+ return ;
+}
+export default KnowledgePage;
diff --git a/react-ui/src/pages/Mirror/Create/index.tsx b/react-ui/src/pages/Mirror/Create/index.tsx
index c4e89ce2..a32e99a1 100644
--- a/react-ui/src/pages/Mirror/Create/index.tsx
+++ b/react-ui/src/pages/Mirror/Create/index.tsx
@@ -30,13 +30,13 @@ type FormData = {
const mirrorRadioItems: KFRadioItem[] = [
{
- key: CommonTabKeys.Public,
title: '基于公网镜像',
+ value: CommonTabKeys.Public,
icon: ,
},
{
- key: CommonTabKeys.Private,
title: '本地上传',
+ value: CommonTabKeys.Private,
icon: ,
},
];
@@ -44,7 +44,7 @@ const mirrorRadioItems: KFRadioItem[] = [
function MirrorCreate() {
const navigate = useNavigate();
const [form] = Form.useForm();
- const [nameDisabled, setNameDisabled] = useState(false);
+ const [isAddVersion, setIsAddVersion] = useState(false); // 是制作镜像还是新增镜像版本
const { message } = App.useApp();
const uploadProps: UploadProps = {
@@ -60,42 +60,47 @@ function MirrorCreate() {
const name = SessionStorage.getItem(SessionStorage.mirrorNameKey);
if (name) {
form.setFieldValue('name', name);
- setNameDisabled(true);
+ setIsAddVersion(true);
}
return () => {
SessionStorage.removeItem(SessionStorage.mirrorNameKey);
};
- }, []);
+ }, [form]);
// 创建公网、本地镜像
const createPublicMirror = async (formData: FormData) => {
const upload_type = formData['upload_type'];
- let params;
+
if (upload_type === CommonTabKeys.Public) {
- params = {
+ const params = {
...omit(formData, ['upload_type']),
upload_type: 0,
image_type: 0,
};
+ const [res] = await to(createMirrorReq(params));
+ if (res) {
+ message.success('创建成功');
+ navigate(-1);
+ }
} else {
const fileList = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const file = fileList[0];
- params = {
+ const params = {
...omit(formData, ['fileList', 'upload_type']),
path: file.response.data.url,
file_size: file.response.data.fileSize,
+ file_name: file.response.data.fileName,
upload_type: 1,
image_type: 0,
};
+ const [res] = await to(createMirrorReq(params));
+ if (res) {
+ message.success('创建成功');
+ navigate(-1);
+ }
}
}
-
- const [res] = await to(createMirrorReq(params));
- if (res) {
- message.success('创建成功');
- navigate(-1);
- }
};
// 提交
@@ -118,14 +123,16 @@ function MirrorCreate() {
return true;
};
+ const descTitle = isAddVersion ? '版本描述' : '镜像描述';
+
return (
-
+
@@ -174,33 +181,33 @@ function MirrorCreate() {
rules={[
{
required: true,
- message: '请输入镜像Tag',
+ message: '请输入镜像版本',
},
{
- pattern: /^[a-zA-Z0-9_-]*$/,
- message: '只支持字母、数字、下划线(_)、中横线(-)',
+ pattern: /^[a-zA-Z0-9._-]+$/,
+ message: '镜像版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
]}
>
-
+
@@ -303,7 +310,7 @@ function MirrorCreate() {
)}