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 + ), + }, +};