diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts
index e9363f91..e8772f6f 100644
--- a/react-ui/config/routes.ts
+++ b/react-ui/config/routes.ts
@@ -176,7 +176,7 @@ export default [
},
{
name: '实验实例详情',
- path: 'instance/:autoMLId/:id',
+ path: 'instance/:experimentId/:id',
component: './AutoML/Instance/index',
},
],
@@ -212,11 +212,47 @@ export default [
},
{
name: '实验实例详情',
- path: 'instance/:autoMLId/:id',
+ path: 'instance/:experimentId/:id',
component: './HyperParameter/Instance/index',
},
],
},
+ {
+ name: '主动学习',
+ path: 'active-learn',
+ routes: [
+ {
+ name: '超参数寻优',
+ path: '',
+ component: './ActiveLearn/List/index',
+ },
+ {
+ name: '实验详情',
+ path: 'info/:id',
+ component: './ActiveLearn/Info/index',
+ },
+ {
+ name: '创建实验',
+ path: 'create',
+ component: './ActiveLearn/Create/index',
+ },
+ {
+ name: '编辑实验',
+ path: 'edit/:id',
+ component: './ActiveLearn/Create/index',
+ },
+ {
+ name: '复制实验',
+ path: 'copy/:id',
+ component: './ActiveLearn/Create/index',
+ },
+ {
+ name: '实验实例详情',
+ path: 'instance/:experimentId/:id',
+ component: './ActiveLearn/Instance/index',
+ },
+ ],
+ },
],
},
{
diff --git a/react-ui/src/components/PageTitle/index.less b/react-ui/src/components/PageTitle/index.less
index 47907246..40913e00 100644
--- a/react-ui/src/components/PageTitle/index.less
+++ b/react-ui/src/components/PageTitle/index.less
@@ -7,4 +7,25 @@
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
+
+ &__tips {
+ position: relative;
+ margin-left: 18px;
+ padding: 3px 15px;
+ color: @primary-color;
+ background: .addAlpha(@primary-color, 0.1) [];
+ 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.1) [];
+ border-bottom: 4px solid transparent;
+ content: '';
+ }
+ }
}
diff --git a/react-ui/src/components/PageTitle/index.tsx b/react-ui/src/components/PageTitle/index.tsx
index 2703e032..113f9b7b 100644
--- a/react-ui/src/components/PageTitle/index.tsx
+++ b/react-ui/src/components/PageTitle/index.tsx
@@ -3,6 +3,7 @@
* @Date: 2024-04-17 14:01:46
* @Description: 页面标题
*/
+import KFIcon from '@/components/KFIcon';
import classNames from 'classnames';
import React from 'react';
import './index.less';
@@ -10,19 +11,29 @@ import './index.less';
type PageTitleProps = {
/** 标题 */
title: React.ReactNode;
+ /** 图标 */
+ tooltip?: string;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
+ /** 自定义标题 */
+ titleRender?: () => React.ReactNode;
};
/**
* 页面标题
*/
-function PageTitle({ title, style, className = '' }: PageTitleProps) {
+function PageTitle({ title, style, className, tooltip }: PageTitleProps) {
return (
- {title}
+
{title}
+ {tooltip && (
+
+
+ {tooltip}
+
+ )}
);
}
diff --git a/react-ui/src/pages/ActiveLearn/Create/index.less b/react-ui/src/pages/ActiveLearn/Create/index.less
new file mode 100644
index 00000000..145be0d1
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/Create/index.less
@@ -0,0 +1,55 @@
+.create-hyperparameter {
+ 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;
+
+ &__type {
+ color: @text-color;
+ font-size: @font-size-input-lg;
+ }
+
+ :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/ActiveLearn/Create/index.tsx b/react-ui/src/pages/ActiveLearn/Create/index.tsx
new file mode 100644
index 00000000..002ad50d
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/Create/index.tsx
@@ -0,0 +1,141 @@
+/*
+ * @Author: 赵伟
+ * @Date: 2024-04-16 13:58:08
+ * @Description: 创建实验
+ */
+import PageTitle from '@/components/PageTitle';
+import {
+ addActiveLearnReq,
+ getActiveLearnInfoReq,
+ updateActiveLearnReq,
+} from '@/services/activeLearn';
+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 { ActiveLearnData, FormData } from '../types';
+import styles from './index.less';
+
+function CreateActiveLearn() {
+ 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(() => {
+ // 编辑,复制
+ if (id && !Number.isNaN(id)) {
+ getActiveLearnInfo(id);
+ }
+ }, [id]);
+
+ // 获取服务详情
+ const getActiveLearnInfo = async (id: number) => {
+ const [res] = await to(getActiveLearnInfoReq({ id }));
+ if (res && res.data) {
+ const info: ActiveLearnData = res.data;
+ const { name: name_str, ...rest } = info;
+ const name = isCopy ? `${name_str}-copy` : name_str;
+ const formData = {
+ ...rest,
+ name,
+ };
+
+ form.setFieldsValue(formData);
+ }
+ };
+
+ // 创建、更新、复制实验
+ const createExperiment = async (formData: FormData) => {
+ // 根据后台要求,修改表单数据
+ const object = {
+ ...formData,
+ };
+
+ const params =
+ id && !isCopy
+ ? {
+ id: id,
+ ...object,
+ }
+ : object;
+
+ const request = id && !isCopy ? updateActiveLearnReq : addActiveLearnReq;
+ 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 CreateActiveLearn;
diff --git a/react-ui/src/pages/ActiveLearn/Info/index.less b/react-ui/src/pages/ActiveLearn/Info/index.less
new file mode 100644
index 00000000..e27756ef
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/Info/index.less
@@ -0,0 +1,40 @@
+.auto-ml-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/ActiveLearn/Info/index.tsx b/react-ui/src/pages/ActiveLearn/Info/index.tsx
new file mode 100644
index 00000000..0091de1f
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/Info/index.tsx
@@ -0,0 +1,45 @@
+/*
+ * @Author: 赵伟
+ * @Date: 2024-04-16 13:58:08
+ * @Description: 主动学习实验详情
+ */
+import PageTitle from '@/components/PageTitle';
+import { getActiveLearnInfoReq } from '@/services/activeLearn';
+import { safeInvoke } from '@/utils/functional';
+import { to } from '@/utils/promise';
+import { useParams } from '@umijs/max';
+import { useEffect, useState } from 'react';
+import BasicInfo from '../components/BasicInfo';
+import { ActiveLearnData } from '../types';
+import styles from './index.less';
+
+function ActiveLearnInfo() {
+ const params = useParams();
+ const id = safeInvoke(Number)(params.id);
+ const [info, setInfo] = useState(undefined);
+
+ useEffect(() => {
+ if (id) {
+ getActiveLearnInfo();
+ }
+ }, []);
+
+ // 获取主动学习详情
+ const getActiveLearnInfo = async () => {
+ const [res] = await to(getActiveLearnInfoReq({ id: id }));
+ if (res && res.data) {
+ setInfo(res.data);
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default ActiveLearnInfo;
diff --git a/react-ui/src/pages/ActiveLearn/Instance/index.less b/react-ui/src/pages/ActiveLearn/Instance/index.less
new file mode 100644
index 00000000..889faeb5
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/Instance/index.less
@@ -0,0 +1,42 @@
+.auto-ml-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: 20px calc(@content-padding - 8px);
+ overflow-y: visible;
+ background-color: white;
+ border-radius: 10px;
+ }
+}
diff --git a/react-ui/src/pages/ActiveLearn/Instance/index.tsx b/react-ui/src/pages/ActiveLearn/Instance/index.tsx
new file mode 100644
index 00000000..5079fc66
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/Instance/index.tsx
@@ -0,0 +1,215 @@
+import KFIcon from '@/components/KFIcon';
+import { AutoMLTaskType, ExperimentStatus } from '@/enums';
+import LogList from '@/pages/Experiment/components/LogList';
+import { getExperimentInsReq } from '@/services/autoML';
+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 BasicInfo from '../components/BasicInfo';
+import ExperimentHistory from '../components/ExperimentHistory';
+import ExperimentResult from '../components/ExperimentResult';
+import { AutoMLInstanceData, HyperparameterData } from '../types';
+import styles from './index.less';
+
+enum TabKeys {
+ Params = 'params',
+ Log = 'log',
+ Result = 'result',
+ History = 'history',
+}
+
+function AutoMLInstance() {
+ const [activeTab, setActiveTab] = useState(TabKeys.Params);
+ const [autoMLInfo, setAutoMLInfo] = useState(undefined);
+ const [instanceInfo, setInstanceInfo] = useState(undefined);
+ const params = useParams();
+ // const autoMLId = safeInvoke(Number)(params.autoMLId);
+ const instanceId = safeInvoke(Number)(params.id);
+ const evtSourceRef = useRef(null);
+
+ useEffect(() => {
+ if (instanceId) {
+ getExperimentInsInfo(false);
+ }
+ return () => {
+ closeSSE();
+ };
+ }, []);
+
+ // 获取实验实例详情
+ const getExperimentInsInfo = async (isStatusDetermined: boolean) => {
+ const [res] = await to(getExperimentInsReq(instanceId));
+ if (res && res.data) {
+ const info = res.data as AutoMLInstanceData;
+ const { param, node_status, argo_ins_name, argo_ins_ns, status } = info;
+ // 解析配置参数
+ const paramJson = parseJsonText(param);
+ if (paramJson) {
+ setAutoMLInfo(paramJson);
+ }
+
+ // 这个接口返回的状态有延时,SSE 返回的状态是最新的
+ // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE
+ if (isStatusDetermined) {
+ setInstanceInfo((prev) => ({
+ ...info,
+ nodeStatus: prev!.nodeStatus,
+ }));
+ return;
+ }
+
+ // 进行节点状态
+ const nodeStatusJson = parseJsonText(node_status);
+ if (nodeStatusJson) {
+ Object.keys(nodeStatusJson).forEach((key) => {
+ if (key.startsWith('auto-ml')) {
+ const value = nodeStatusJson[key];
+ info.nodeStatus = value;
+ }
+ });
+ }
+ setInstanceInfo(info);
+ // 运行中或者等待中,开启 SSE
+ if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) {
+ setupSSE(argo_ins_name, argo_ins_ns);
+ }
+ }
+ };
+
+ const setupSSE = (name: string, namespace: string) => {
+ let { origin } = location;
+ if (process.env.NODE_ENV === 'development') {
+ origin = 'http://172.20.32.181:31213';
+ }
+ 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) {
+ const statusData = Object.values(nodes).find((node: any) =>
+ node.displayName.startsWith('auto-ml'),
+ ) as NodeStatus;
+ if (statusData) {
+ setInstanceInfo((prev) => ({
+ ...prev!,
+ nodeStatus: statusData,
+ }));
+
+ // 实验结束,关闭 SSE
+ if (
+ statusData.phase !== ExperimentStatus.Pending &&
+ statusData.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 && instanceInfo.nodeStatus && (
+
+ )}
+
+ ),
+ },
+ ];
+
+ const resultTabItems = [
+ {
+ key: TabKeys.Result,
+ label: '实验结果',
+ icon: ,
+ children: (
+
+ ),
+ },
+ {
+ key: TabKeys.History,
+ label: 'Trial 列表',
+ icon: ,
+ children: (
+
+ ),
+ },
+ ];
+
+ const tabItems =
+ instanceInfo?.status === ExperimentStatus.Succeeded
+ ? [...basicTabItems, ...resultTabItems]
+ : basicTabItems;
+
+ return (
+
+
+
+ );
+}
+
+export default AutoMLInstance;
diff --git a/react-ui/src/pages/ActiveLearn/List/index.tsx b/react-ui/src/pages/ActiveLearn/List/index.tsx
new file mode 100644
index 00000000..01e316bd
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/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 ActiveLearn() {
+ return ;
+}
+
+export default ActiveLearn;
diff --git a/react-ui/src/pages/ActiveLearn/components/BasicInfo/index.less b/react-ui/src/pages/ActiveLearn/components/BasicInfo/index.less
new file mode 100644
index 00000000..f365aa66
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/BasicInfo/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/ActiveLearn/components/BasicInfo/index.tsx b/react-ui/src/pages/ActiveLearn/components/BasicInfo/index.tsx
new file mode 100644
index 00000000..5388d5f7
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/BasicInfo/index.tsx
@@ -0,0 +1,194 @@
+import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
+import {
+ classifierAlgorithms,
+ performanceMetrics,
+ queryStrategies,
+ stoppingCriterions,
+ StoppingCriterionsType,
+} from '@/pages/ActiveLearn/components/CreateForm/utils';
+import { ActiveLearnData } from '@/pages/ActiveLearn/types';
+import { experimentStatusInfo } from '@/pages/Experiment/status';
+import { type NodeStatus } from '@/types';
+import { elapsedTime } from '@/utils/date';
+import { formatDataset, formatDate, formatEnum } from '@/utils/format';
+import { Flex } from 'antd';
+import classNames from 'classnames';
+import { useMemo } from 'react';
+import styles from './index.less';
+
+type BasicInfoProps = {
+ info?: ActiveLearnData;
+ className?: string;
+ isInstance?: boolean;
+ runStatus?: NodeStatus;
+};
+
+function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfoProps) {
+ 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 [];
+ }
+ let stopping_criterion_params_title = '';
+ let stopping_criterion_params_value: number | undefined = undefined;
+ if (info.stopping_criterion === StoppingCriterionsType.NumOfQueries) {
+ stopping_criterion_params_title = '查询次数';
+ stopping_criterion_params_value = info.num_of_queries;
+ } else if (info.stopping_criterion === StoppingCriterionsType.PercentOfUnlabel) {
+ stopping_criterion_params_title = '未标记比例';
+ stopping_criterion_params_value = info.percent_of_unlabel;
+ } else {
+ stopping_criterion_params_title = '时间限制';
+ stopping_criterion_params_value = info.time_limit;
+ }
+
+ return [
+ {
+ label: '分类算法',
+ value: info.classifier_type,
+ format: formatEnum(classifierAlgorithms),
+ },
+ {
+ label: '停止判则',
+ value: info.stopping_criterion,
+ format: formatEnum(stoppingCriterions),
+ },
+ {
+ label: stopping_criterion_params_title,
+ value: stopping_criterion_params_value,
+ },
+ {
+ label: '查询策略',
+ value: info.query_strategy,
+ format: formatEnum(queryStrategies),
+ },
+ {
+ label: '试验次数',
+ value: info.num_of_experiment,
+ },
+ {
+ label: '指标',
+ value: info.performance_metric,
+ format: formatEnum(performanceMetrics),
+ },
+ {
+ label: '数据集',
+ value: info.dataset,
+ format: formatDataset,
+ },
+ {
+ label: '预测目标列',
+ value: info.target_columns,
+ },
+ {
+ label: '测试集比率',
+ value: info.test_ratio,
+ },
+ {
+ label: '初始标记数据比率',
+ value: info.initial_label_rate,
+ },
+ ];
+ }, [info]);
+
+ const instanceDatas = useMemo(() => {
+ if (!runStatus) {
+ return [];
+ }
+
+ return [
+ {
+ label: '启动时间',
+ value: formatDate(runStatus.startedAt),
+ ellipsis: true,
+ },
+ {
+ label: '执行时长',
+ value: elapsedTime(runStatus.startedAt, runStatus.finishedAt),
+ ellipsis: true,
+ },
+ {
+ label: '状态',
+ value: (
+
+
+
+ {experimentStatusInfo[runStatus?.phase]?.label}
+
+
+ ),
+ ellipsis: true,
+ },
+ ];
+ }, [runStatus]);
+
+ return (
+
+ {isInstance && runStatus && (
+
+ )}
+ {!isInstance && (
+
+ )}
+
+
+ );
+}
+
+export default BasicInfo;
diff --git a/react-ui/src/pages/ActiveLearn/components/CreateForm/BasicConfig.tsx b/react-ui/src/pages/ActiveLearn/components/CreateForm/BasicConfig.tsx
new file mode 100644
index 00000000..8829f12a
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/CreateForm/BasicConfig.tsx
@@ -0,0 +1,54 @@
+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/ActiveLearn/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx
new file mode 100644
index 00000000..8e7bda92
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx
@@ -0,0 +1,262 @@
+import ResourceSelect, {
+ requiredValidator,
+ ResourceSelectorType,
+} from '@/components/ResourceSelect';
+import SubAreaTitle from '@/components/SubAreaTitle';
+import { Col, Form, Input, InputNumber, Row, Select } from 'antd';
+import {
+ classifierAlgorithms,
+ performanceMetrics,
+ queryStrategies,
+ stoppingCriterions,
+ StoppingCriterionsType,
+} from './utils';
+
+function ExecuteConfig() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {({ getFieldValue }) => {
+ const stopping_criterion = getFieldValue('stopping_criterion');
+ if (stopping_criterion === StoppingCriterionsType.NumOfQueries) {
+ return (
+
+
+
+
+
+
+
+ );
+ } else if (stopping_criterion === StoppingCriterionsType.PercentOfUnlabel) {
+ return (
+
+
+
+
+
+
+
+ );
+ } else if (stopping_criterion === StoppingCriterionsType.TimeLimit) {
+ return (
+
+
+
+
+
+
+
+ );
+ } else {
+ return null;
+ }
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default ExecuteConfig;
diff --git a/react-ui/src/pages/ActiveLearn/components/CreateForm/index.less b/react-ui/src/pages/ActiveLearn/components/CreateForm/index.less
new file mode 100644
index 00000000..06bbd5b7
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/CreateForm/index.less
@@ -0,0 +1,145 @@
+.metrics-weight {
+ margin-bottom: 20px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.add-weight {
+ margin-bottom: 0 !important;
+
+ // 增加样式权重
+ & &__button {
+ 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/ActiveLearn/components/CreateForm/utils.ts b/react-ui/src/pages/ActiveLearn/components/CreateForm/utils.ts
new file mode 100644
index 00000000..c7fd8567
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/CreateForm/utils.ts
@@ -0,0 +1,97 @@
+// 分类算法
+export const classifierAlgorithms = [
+ {
+ label: 'logistic_regression(逻辑回归)',
+ value: 'logistic_regression',
+ },
+ {
+ label: 'decision_tree(决策树)',
+ value: 'decision_tree',
+ },
+ {
+ label: 'random_forest(随机森林)',
+ value: 'random_forest',
+ },
+ {
+ label: 'SVM(支持向量机)',
+ value: 'SVM',
+ },
+ {
+ label: 'naive_bayes(朴素贝叶斯)',
+ value: 'naive_bayes',
+ },
+ {
+ label: 'GBM(梯度提升树)',
+ value: 'GBM',
+ },
+];
+
+export enum StoppingCriterionsType {
+ NumOfQueries = 'num_of_queries',
+ PercentOfUnlabel = 'percent_of_unlabel',
+ TimeLimit = 'time_limit',
+}
+
+// 停止判则
+export const stoppingCriterions = [
+ {
+ label: 'num_of_queries(查询次数)',
+ value: 'num_of_queries',
+ },
+ {
+ label: 'percent_of_unlabel(未标记样本比例)',
+ value: 'percent_of_unlabel',
+ },
+ {
+ label: 'time_limit(时间限制)',
+ value: 'time_limit',
+ },
+];
+
+// 查询策略
+export const queryStrategies = [
+ {
+ label: 'Uncertainty(不确定性)',
+ value: 'Uncertainty',
+ },
+ {
+ label: 'QBC(委员会查询)',
+ value: 'QBC',
+ },
+ {
+ label: 'Random(随机)',
+ value: 'Random',
+ },
+ {
+ label: 'GraphDensity(图密度)',
+ value: 'GraphDensity',
+ },
+];
+
+// 指标
+export const performanceMetrics = [
+ {
+ label: 'accuracy_score',
+ value: 'accuracy_score',
+ },
+ {
+ label: 'roc_auc_score',
+ value: 'roc_auc_score',
+ },
+ {
+ label: 'get_fps_tps_thresholds',
+ value: 'get_fps_tps_thresholds',
+ },
+ {
+ label: 'hamming_loss',
+ value: 'hamming_loss',
+ },
+ {
+ label: 'one_error',
+ value: 'one_error',
+ },
+ {
+ label: 'coverage_error',
+ value: 'coverage_error',
+ },
+];
diff --git a/react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.less b/react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.less
new file mode 100644
index 00000000..beac2a8a
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.less
@@ -0,0 +1,14 @@
+.experiment-history {
+ height: calc(100% - 10px);
+ margin-top: 10px;
+ &__content {
+ height: 100%;
+ padding: 20px @content-padding;
+ background-color: white;
+ border-radius: 10px;
+
+ &__table {
+ height: 100%;
+ }
+ }
+}
diff --git a/react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.tsx b/react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.tsx
new file mode 100644
index 00000000..e95ccd42
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.tsx
@@ -0,0 +1,132 @@
+import { getFileReq } from '@/services/file';
+import { to } from '@/utils/promise';
+import tableCellRender from '@/utils/table';
+import { Table, type TableProps } from 'antd';
+import classNames from 'classnames';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+type ExperimentHistoryProps = {
+ fileUrl?: string;
+ isClassification: boolean;
+};
+
+type TableData = {
+ id?: string;
+ accuracy?: number;
+ duration?: number;
+ train_loss?: number;
+ status?: string;
+ feature?: string;
+ althorithm?: string;
+};
+
+function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) {
+ const [tableData, setTableData] = useState([]);
+ useEffect(() => {
+ if (fileUrl) {
+ getHistoryFile();
+ }
+ }, [fileUrl]);
+
+ // 获取实验运行历史记录
+ const getHistoryFile = async () => {
+ const [res] = await to(getFileReq(fileUrl));
+ if (res) {
+ const data: any[] = res.data;
+ const list: TableData[] = data.map((item) => {
+ return {
+ id: item[0]?.[0],
+ accuracy: item[1]?.[5]?.accuracy,
+ duration: item[1]?.[5]?.duration,
+ train_loss: item[1]?.[5]?.train_loss,
+ status: item[1]?.[2]?.['__enum__']?.split('.')?.[1],
+ };
+ });
+ list.forEach((item) => {
+ if (!item.id) return;
+ const config = (res as any).configs?.[item.id];
+ item.feature = config?.['feature_preprocessor:__choice__'];
+ item.althorithm = isClassification
+ ? config?.['classifier:__choice__']
+ : config?.['regressor:__choice__'];
+ });
+ setTableData(list);
+ }
+ };
+
+ const columns: TableProps['columns'] = [
+ {
+ title: 'ID',
+ dataIndex: 'id',
+ key: 'id',
+ width: 80,
+ render: tableCellRender(false),
+ },
+ {
+ title: '准确率',
+ dataIndex: 'accuracy',
+ key: 'accuracy',
+ render: tableCellRender(true),
+ ellipsis: { showTitle: false },
+ },
+ {
+ title: '耗时',
+ dataIndex: 'duration',
+ key: 'duration',
+ render: tableCellRender(true),
+ ellipsis: { showTitle: false },
+ },
+ {
+ title: '训练损失',
+ dataIndex: 'train_loss',
+ key: 'train_loss',
+ render: tableCellRender(true),
+ ellipsis: { showTitle: false },
+ },
+ {
+ title: '特征处理',
+ dataIndex: 'feature',
+ key: 'feature',
+ render: tableCellRender(true),
+ ellipsis: { showTitle: false },
+ },
+ {
+ title: '算法',
+ dataIndex: 'althorithm',
+ key: 'althorithm',
+ render: tableCellRender(true),
+ ellipsis: { showTitle: false },
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 120,
+ render: tableCellRender(false),
+ },
+ ];
+
+ return (
+
+ );
+}
+
+export default ExperimentHistory;
diff --git a/react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.less b/react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.less
new file mode 100644
index 00000000..e69de29b
diff --git a/react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.tsx b/react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.less b/react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.less
new file mode 100644
index 00000000..342817c3
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.less
@@ -0,0 +1,52 @@
+.experiment-result {
+ height: calc(100% - 10px);
+ margin-top: 10px;
+ padding: 20px @content-padding;
+ overflow-y: auto;
+ background-color: white;
+ border-radius: 10px;
+
+ &__download {
+ padding-top: 16px;
+ padding-bottom: 16px;
+
+ padding-left: @content-padding;
+ color: @text-color;
+ font-size: 13px;
+ background-color: #f8f8f9;
+ border-radius: 4px;
+
+ &__btn {
+ display: block;
+ height: 36px;
+ margin-top: 15px;
+ font-size: 14px;
+ }
+ }
+
+ &__text {
+ white-space: pre-wrap;
+ }
+
+ &__images {
+ display: flex;
+ align-items: flex-start;
+ width: 100%;
+ overflow-x: auto;
+
+ :global {
+ .ant-image {
+ margin-right: 20px;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+
+ &__item {
+ height: 248px;
+ border: 1px solid rgba(96, 107, 122, 0.3);
+ }
+ }
+}
diff --git a/react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.tsx b/react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.tsx
new file mode 100644
index 00000000..a826155d
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.tsx
@@ -0,0 +1,83 @@
+import InfoGroup from '@/components/InfoGroup';
+import { getFileReq } from '@/services/file';
+import { to } from '@/utils/promise';
+import { Button, Image } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import styles from './index.less';
+
+type ExperimentResultProps = {
+ fileUrl?: string;
+ imageUrl?: string;
+ modelPath?: string;
+};
+
+function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) {
+ const [result, setResult] = useState('');
+
+ const images = useMemo(() => {
+ if (imageUrl) {
+ return imageUrl.split(',').map((item) => item.trim());
+ }
+ return [];
+ }, [imageUrl]);
+
+ useEffect(() => {
+ if (fileUrl) {
+ getResultFile();
+ }
+ }, [fileUrl]);
+
+ // 获取实验运行历史记录
+ const getResultFile = async () => {
+ const [res] = await to(getFileReq(fileUrl));
+ if (res) {
+ setResult(res as any as string);
+ }
+ };
+
+ return (
+
+
+ {result}
+
+
+
+
+ console.log(`current index: ${current}, prev index: ${prev}`),
+ }}
+ >
+ {images.map((item) => (
+
+ ))}
+
+
+
+ {modelPath && (
+
+ 文件名
+ save_model.joblib
+
+
+ )}
+
+ );
+}
+
+export default ExperimentResult;
diff --git a/react-ui/src/pages/ActiveLearn/types.ts b/react-ui/src/pages/ActiveLearn/types.ts
new file mode 100644
index 00000000..b7421d92
--- /dev/null
+++ b/react-ui/src/pages/ActiveLearn/types.ts
@@ -0,0 +1,61 @@
+import { type ParameterInputObject } from '@/components/ResourceSelect';
+import { type NodeStatus } from '@/types';
+
+// 操作类型
+export enum OperationType {
+ Create = 'Create', // 创建
+ Update = 'Update', // 更新
+}
+
+// 表单数据
+export type FormData = {
+ name: string; // 实验名称
+ description: string; // 实验描述
+ dataset: ParameterInputObject; // 数据集
+ classifier_type: string; // 分类算法
+ stopping_criterion: string; // 停止判则
+ query_strategy: string; // 查询策略
+ num_of_experiment: number; // 试验次数
+ performance_metric: string; // 指标
+ target_columns: string; // 预测目标列
+ test_ratio: number; // 测试集比率
+ initial_label_rate: number; // 初始标记数据比率
+ num_of_queries?: number; // 查询次数
+ percent_of_unlabel: number; // 未标记比例
+ time_limit: number; // 时间限制
+};
+
+// 主动学习
+export type ActiveLearnData = {
+ 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 ActiveLearnInstanceData = {
+ id: number;
+ auto_ml_id: number;
+ result_path: string;
+ model_path: string;
+ img_path: string;
+ run_history_path: 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;
+};
diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx
index 947ac63d..af68bfd3 100644
--- a/react-ui/src/pages/AutoML/Instance/index.tsx
+++ b/react-ui/src/pages/AutoML/Instance/index.tsx
@@ -27,7 +27,6 @@ function AutoMLInstance() {
const [autoMLInfo, setAutoMLInfo] = useState(undefined);
const [instanceInfo, setInstanceInfo] = useState(undefined);
const params = useParams();
- // const autoMLId = safeInvoke(Number)(params.autoMLId);
const instanceId = safeInvoke(Number)(params.id);
const evtSourceRef = useRef(null);
diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/config.ts b/react-ui/src/pages/AutoML/components/ExperimentList/config.ts
index 0a748520..adbd2789 100644
--- a/react-ui/src/pages/AutoML/components/ExperimentList/config.ts
+++ b/react-ui/src/pages/AutoML/components/ExperimentList/config.ts
@@ -4,6 +4,15 @@
* @Description: 实验列表组件配置
*/
+import {
+ batchDeleteActiveLearnInsReq,
+ deleteActiveLearnInsReq,
+ deleteActiveLearnReq,
+ getActiveLearnInsListReq,
+ getActiveLearnListReq,
+ runActiveLearnReq,
+ stopActiveLearnInsReq,
+} from '@/services/activeLearn';
import {
batchDeleteExperimentInsReq,
deleteAutoMLReq,
@@ -26,6 +35,7 @@ import {
export enum ExperimentListType {
AutoML = 'AutoML',
HyperParameter = 'HyperParameter',
+ ActiveLearn = 'ActiveLearn',
}
type ExperimentListInfo = {
@@ -72,4 +82,18 @@ export const experimentListConfig: Record
+ ),
+ },
+};