| @@ -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', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -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: ''; | |||
| } | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <div className={classNames('kf-page-title', className)} style={style}> | |||
| {title} | |||
| <div>{title}</div> | |||
| {tooltip && ( | |||
| <div className="kf-page-title__tips"> | |||
| <KFIcon type="icon-tishi" font={14} /> | |||
| <span style={{ marginLeft: '8px', fontSize: '14px' }}>{tooltip}</span> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <div className={styles['create-hyperparameter']}> | |||
| <PageTitle title={title} tooltip="仅支持二分类及多分类任务"></PageTitle> | |||
| <div className={styles['create-hyperparameter__content']}> | |||
| <div> | |||
| <Form | |||
| name="create-active-learn" | |||
| labelCol={{ flex: '160px' }} | |||
| labelAlign="left" | |||
| form={form} | |||
| onFinish={handleSubmit} | |||
| size="large" | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| initialValues={{ | |||
| test_ratio: 0.3, | |||
| initial_label_rate: 0.1, | |||
| }} | |||
| > | |||
| <BasicConfig /> | |||
| <ExecuteConfig /> | |||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }} style={{ marginTop: '40px' }}> | |||
| <Button type="primary" htmlType="submit"> | |||
| {buttonText} | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| htmlType="button" | |||
| onClick={cancel} | |||
| style={{ marginLeft: '20px' }} | |||
| > | |||
| 取消 | |||
| </Button> | |||
| </Form.Item> | |||
| </Form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default CreateActiveLearn; | |||
| @@ -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: ''; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<ActiveLearnData | undefined>(undefined); | |||
| useEffect(() => { | |||
| if (id) { | |||
| getActiveLearnInfo(); | |||
| } | |||
| }, []); | |||
| // 获取主动学习详情 | |||
| const getActiveLearnInfo = async () => { | |||
| const [res] = await to(getActiveLearnInfoReq({ id: id })); | |||
| if (res && res.data) { | |||
| setInfo(res.data); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles['auto-ml-info']}> | |||
| <PageTitle title="实验详情"></PageTitle> | |||
| <div className={styles['auto-ml-info__content']}> | |||
| <BasicInfo info={info} /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ActiveLearnInfo; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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<string>(TabKeys.Params); | |||
| const [autoMLInfo, setAutoMLInfo] = useState<HyperparameterData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||
| const params = useParams(); | |||
| // const autoMLId = safeInvoke(Number)(params.autoMLId); | |||
| const instanceId = safeInvoke(Number)(params.id); | |||
| const evtSourceRef = useRef<EventSource | null>(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: <KFIcon type="icon-jibenxinxi" />, | |||
| children: ( | |||
| <BasicInfo | |||
| className={styles['auto-ml-instance__basic']} | |||
| info={autoMLInfo} | |||
| runStatus={instanceInfo?.nodeStatus} | |||
| isInstance | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.Log, | |||
| label: '日志', | |||
| icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['auto-ml-instance__log']}> | |||
| {instanceInfo && instanceInfo.nodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={instanceInfo.nodeStatus.displayName} | |||
| workflowId={instanceInfo.nodeStatus.id} | |||
| instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} | |||
| instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| const resultTabItems = [ | |||
| { | |||
| key: TabKeys.Result, | |||
| label: '实验结果', | |||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | |||
| children: ( | |||
| <ExperimentResult | |||
| fileUrl={instanceInfo?.result_path} | |||
| imageUrl={instanceInfo?.img_path} | |||
| modelPath={instanceInfo?.model_path} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: 'Trial 列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| fileUrl={instanceInfo?.run_history_path} | |||
| isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} | |||
| /> | |||
| ), | |||
| }, | |||
| ]; | |||
| const tabItems = | |||
| instanceInfo?.status === ExperimentStatus.Succeeded | |||
| ? [...basicTabItems, ...resultTabItems] | |||
| : basicTabItems; | |||
| return ( | |||
| <div className={styles['auto-ml-instance']}> | |||
| <Tabs | |||
| className={styles['auto-ml-instance__tabs']} | |||
| items={tabItems} | |||
| activeKey={activeTab} | |||
| onChange={setActiveTab} | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLInstance; | |||
| @@ -0,0 +1,13 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 超参数自动寻优 | |||
| */ | |||
| import ExperimentList, { ExperimentListType } from '@/pages/AutoML/components/ExperimentList'; | |||
| function ActiveLearn() { | |||
| return <ExperimentList type={ExperimentListType.ActiveLearn} />; | |||
| } | |||
| export default ActiveLearn; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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: ( | |||
| <Flex align="center"> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={experimentStatusInfo[runStatus.phase]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div | |||
| style={{ | |||
| color: experimentStatusInfo[runStatus?.phase]?.color, | |||
| fontSize: '15px', | |||
| lineHeight: 1.6, | |||
| }} | |||
| > | |||
| {experimentStatusInfo[runStatus?.phase]?.label} | |||
| </div> | |||
| </Flex> | |||
| ), | |||
| ellipsis: true, | |||
| }, | |||
| ]; | |||
| }, [runStatus]); | |||
| return ( | |||
| <div className={classNames(styles['hyper-parameter-basic'], className)}> | |||
| {isInstance && runStatus && ( | |||
| <ConfigInfo | |||
| title="运行信息" | |||
| datas={instanceDatas} | |||
| labelWidth={70} | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| title="基本信息" | |||
| datas={basicDatas} | |||
| labelWidth={70} | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| )} | |||
| <ConfigInfo | |||
| title="配置信息" | |||
| datas={configDatas} | |||
| labelWidth={120} | |||
| style={{ marginBottom: '20px' }} | |||
| ></ConfigInfo> | |||
| </div> | |||
| ); | |||
| } | |||
| export default BasicInfo; | |||
| @@ -0,0 +1,54 @@ | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { Col, Form, Input, Row } from 'antd'; | |||
| function BasicConfig() { | |||
| return ( | |||
| <> | |||
| <SubAreaTitle | |||
| title="基本信息" | |||
| image={require('@/assets/img/mirror-basic.png')} | |||
| style={{ marginBottom: '26px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="实验名称" | |||
| name="name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入实验名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入实验名称" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={20}> | |||
| <Form.Item | |||
| label="实验描述" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入实验描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| placeholder="请输入实验描述" | |||
| maxLength={256} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| } | |||
| export default BasicConfig; | |||
| @@ -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 ( | |||
| <> | |||
| <SubAreaTitle | |||
| title="配置信息" | |||
| image={require('@/assets/img/model-deployment.png')} | |||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="分类算法" | |||
| name="classifier_type" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请选择分类算法', | |||
| }, | |||
| ]} | |||
| > | |||
| <Select | |||
| placeholder="请选择分类算法" | |||
| options={classifierAlgorithms} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="停止判则" | |||
| name="stopping_criterion" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请选择停止判则', | |||
| }, | |||
| ]} | |||
| > | |||
| <Select | |||
| placeholder="请选择停止判则" | |||
| options={stoppingCriterions} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Form.Item dependencies={['stopping_criterion']} noStyle> | |||
| {({ getFieldValue }) => { | |||
| const stopping_criterion = getFieldValue('stopping_criterion'); | |||
| if (stopping_criterion === StoppingCriterionsType.NumOfQueries) { | |||
| return ( | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="查询次数" | |||
| name="num_of_queries" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入查询次数', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入查询次数" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| ); | |||
| } else if (stopping_criterion === StoppingCriterionsType.PercentOfUnlabel) { | |||
| return ( | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="未标记比例" | |||
| name="percent_of_unlabel" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入未标记比例', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入未标记比例" min={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| ); | |||
| } else if (stopping_criterion === StoppingCriterionsType.TimeLimit) { | |||
| return ( | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="时间限制" | |||
| name="time_limit" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入时间限制', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入时间限制" min={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| ); | |||
| } else { | |||
| return null; | |||
| } | |||
| }} | |||
| </Form.Item> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="查询策略" | |||
| name="query_strategy" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请选择查询策略', | |||
| }, | |||
| ]} | |||
| > | |||
| <Select placeholder="请选择查询策略" options={queryStrategies} showSearch allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="试验次数" | |||
| name="num_of_experiment" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入试验次数', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入试验次数" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="指标" | |||
| name="performance_metric" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请选择指标', | |||
| }, | |||
| ]} | |||
| > | |||
| <Select placeholder="请选择指标" options={performanceMetrics} showSearch allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="数据集" | |||
| name="dataset" | |||
| rules={[ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '请选择数据集', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Dataset} | |||
| placeholder="请选择数据集" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="预测目标列" | |||
| name="target_columns" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入预测目标列', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入预测目标列" maxLength={256} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="测试集比率" | |||
| name="test_ratio" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入测试集比率', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入测试集比率" min={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="初始标记数据比率" | |||
| name="initial_label_rate" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入初始标记数据比率', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入初始标记数据比率" min={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| } | |||
| export default ExecuteConfig; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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', | |||
| }, | |||
| ]; | |||
| @@ -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%; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<TableData[]>([]); | |||
| 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<TableData>['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 ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| <div | |||
| className={classNames( | |||
| 'vertical-scroll-table-no-page', | |||
| styles['experiment-history__content__table'], | |||
| )} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| pagination={false} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentHistory; | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<string | undefined>(''); | |||
| 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 ( | |||
| <div className={styles['experiment-result']}> | |||
| <InfoGroup title="实验结果" height={420} width="100%"> | |||
| <div className={styles['experiment-result__text']}>{result}</div> | |||
| </InfoGroup> | |||
| <InfoGroup title="可视化结果" style={{ margin: '16px 0' }}> | |||
| <div className={styles['experiment-result__images']}> | |||
| <Image.PreviewGroup | |||
| preview={{ | |||
| onChange: (current, prev) => | |||
| console.log(`current index: ${current}, prev index: ${prev}`), | |||
| }} | |||
| > | |||
| {images.map((item) => ( | |||
| <Image | |||
| key={item} | |||
| className={styles['experiment-result__images__item']} | |||
| src={item} | |||
| height={248} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| ))} | |||
| </Image.PreviewGroup> | |||
| </div> | |||
| </InfoGroup> | |||
| {modelPath && ( | |||
| <div className={styles['experiment-result__download']}> | |||
| <span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span> | |||
| <span>save_model.joblib</span> | |||
| <Button | |||
| type="primary" | |||
| className={styles['experiment-result__download__btn']} | |||
| onClick={() => { | |||
| window.location.href = modelPath; | |||
| }} | |||
| > | |||
| 模型下载 | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentResult; | |||
| @@ -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; | |||
| }; | |||
| @@ -27,7 +27,6 @@ function AutoMLInstance() { | |||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||
| const params = useParams(); | |||
| // const autoMLId = safeInvoke(Number)(params.autoMLId); | |||
| const instanceId = safeInvoke(Number)(params.id); | |||
| const evtSourceRef = useRef<EventSource | null>(null); | |||
| @@ -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<ExperimentListType, ExperimentListInfo | |||
| descProperty: 'description', | |||
| idProperty: 'rayId', | |||
| }, | |||
| [ExperimentListType.ActiveLearn]: { | |||
| getListReq: getActiveLearnListReq, | |||
| getInsListReq: getActiveLearnInsListReq, | |||
| deleteRecordReq: deleteActiveLearnReq, | |||
| runRecordReq: runActiveLearnReq, | |||
| deleteInsReq: deleteActiveLearnInsReq, | |||
| batchDeleteInsReq: batchDeleteActiveLearnInsReq, | |||
| stopInsReq: stopActiveLearnInsReq, | |||
| title: '自动学习', | |||
| pathPrefix: 'active-learn', | |||
| nameProperty: 'name', | |||
| descProperty: 'description', | |||
| idProperty: 'activeLearnId', | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,93 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-11-18 10:18:27 | |||
| * @Description: 主动学习 | |||
| */ | |||
| import { request } from '@umijs/max'; | |||
| // 分页查询超参数自动寻优 | |||
| export function getActiveLearnListReq(params) { | |||
| return request(`/api/mmp/activeLearn`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 查询超参数自动寻优详情 | |||
| export function getActiveLearnInfoReq(params) { | |||
| return request(`/api/mmp/activeLearn/getActiveLearnDetail`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 新增超参数自动寻优 | |||
| export function addActiveLearnReq(data) { | |||
| return request(`/api/mmp/activeLearn`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| // 编辑超参数自动寻优 | |||
| export function updateActiveLearnReq(data) { | |||
| return request(`/api/mmp/activeLearn`, { | |||
| method: 'PUT', | |||
| data, | |||
| }); | |||
| } | |||
| // 删除超参数自动寻优 | |||
| export function deleteActiveLearnReq(id) { | |||
| return request(`/api/mmp/activeLearn/${id}`, { | |||
| method: 'DELETE', | |||
| }); | |||
| } | |||
| // 运行超参数自动寻优 | |||
| export function runActiveLearnReq(id) { | |||
| return request(`/api/mmp/activeLearn/run/${id}`, { | |||
| method: 'POST', | |||
| }); | |||
| } | |||
| // ----------------------- 实验实例 ----------------------- | |||
| // 获取实验实例列表 | |||
| export function getActiveLearnInsListReq(params) { | |||
| return request(`/api/mmp/activeLearnIns`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 查询实验实例详情 | |||
| export function getActiveLearnInsReq(id) { | |||
| return request(`/api/mmp/activeLearnIns/${id}`, { | |||
| method: 'GET', | |||
| }); | |||
| } | |||
| // 停止实验实例 | |||
| export function stopActiveLearnInsReq(id) { | |||
| return request(`/api/mmp/activeLearnIns/${id}`, { | |||
| method: 'PUT', | |||
| }); | |||
| } | |||
| // 删除实验实例 | |||
| export function deleteActiveLearnInsReq(id) { | |||
| return request(`/api/mmp/activeLearnIns/${id}`, { | |||
| method: 'DELETE', | |||
| }); | |||
| } | |||
| // 批量删除实验实例 | |||
| export function batchDeleteActiveLearnInsReq(data) { | |||
| return request(`/api/mmp/activeLearnIns/batchDelete`, { | |||
| method: 'DELETE', | |||
| data | |||
| }); | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import type { Meta, StoryObj } from '@storybook/react'; | |||
| import { Tabs } from 'antd'; | |||
| // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | |||
| const meta = { | |||
| @@ -28,3 +29,25 @@ export const Primary: Story = { | |||
| title: '数据集列表', | |||
| }, | |||
| }; | |||
| /* 带有提示信息 */ | |||
| export const WithTooltip: Story = { | |||
| args: { | |||
| title: '数据集列表', | |||
| tooltip: '其它提示', | |||
| }, | |||
| }; | |||
| /* Title 可以是 ReactNode */ | |||
| export const Custom: Story = { | |||
| args: { | |||
| title: ( | |||
| <Tabs | |||
| items={[ | |||
| { label: 'Tab 1', key: '1' }, | |||
| { label: 'Tab 2', key: '2' }, | |||
| ]} | |||
| ></Tabs> | |||
| ), | |||
| }, | |||
| }; | |||