| @@ -181,6 +181,42 @@ export default [ | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| name: '超参数自动寻优', | |||||
| path: 'hyperparameter', | |||||
| routes: [ | |||||
| { | |||||
| name: '超参数寻优', | |||||
| path: '', | |||||
| component: './HyperParameter/List/index', | |||||
| }, | |||||
| { | |||||
| name: '实验详情', | |||||
| path: 'info/:id', | |||||
| component: './HyperParameter/Info/index', | |||||
| }, | |||||
| { | |||||
| name: '创建实验', | |||||
| path: 'create', | |||||
| component: './HyperParameter/Create/index', | |||||
| }, | |||||
| { | |||||
| name: '编辑实验', | |||||
| path: 'edit/:id', | |||||
| component: './HyperParameter/Create/index', | |||||
| }, | |||||
| { | |||||
| name: '复制实验', | |||||
| path: 'copy/:id', | |||||
| component: './HyperParameter/Create/index', | |||||
| }, | |||||
| { | |||||
| name: '实验实例详情', | |||||
| path: 'instance/:autoMLId/:id', | |||||
| component: './HyperParameter/Instance/index', | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 创建服务版本 | |||||
| * @Description: 创建实验 | |||||
| */ | */ | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums'; | import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums'; | ||||
| @@ -11,7 +11,6 @@ import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { useLocation, useNavigate, useParams } from '@umijs/max'; | import { useLocation, useNavigate, useParams } from '@umijs/max'; | ||||
| import { App, Button, Form } from 'antd'; | import { App, Button, Form } from 'antd'; | ||||
| import { omit } from 'lodash'; | |||||
| import { useEffect } from 'react'; | import { useEffect } from 'react'; | ||||
| import BasicConfig from '../components/CreateForm/BasicConfig'; | import BasicConfig from '../components/CreateForm/BasicConfig'; | ||||
| import DatasetConfig from '../components/CreateForm/DatasetConfig'; | import DatasetConfig from '../components/CreateForm/DatasetConfig'; | ||||
| @@ -106,7 +105,7 @@ function CreateAutoML() { | |||||
| // 根据后台要求,修改表单数据 | // 根据后台要求,修改表单数据 | ||||
| const object = { | const object = { | ||||
| ...omit(formData), | |||||
| ...formData, | |||||
| include_classifier: convertEmptyStringToUndefined(include_classifier), | include_classifier: convertEmptyStringToUndefined(include_classifier), | ||||
| include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor), | include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor), | ||||
| include_regressor: convertEmptyStringToUndefined(include_regressor), | include_regressor: convertEmptyStringToUndefined(include_regressor), | ||||
| @@ -3,419 +3,11 @@ | |||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 自主机器学习列表 | * @Description: 自主机器学习列表 | ||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { | |||||
| deleteAutoMLReq, | |||||
| getAutoMLListReq, | |||||
| getExperimentInsListReq, | |||||
| runAutoMLReq, | |||||
| } from '@/services/autoML'; | |||||
| import themes from '@/styles/theme.less'; | |||||
| import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { | |||||
| App, | |||||
| Button, | |||||
| ConfigProvider, | |||||
| Input, | |||||
| Table, | |||||
| Tooltip, | |||||
| type TablePaginationConfig, | |||||
| type TableProps, | |||||
| } from 'antd'; | |||||
| import { type SearchProps } from 'antd/es/input'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import ExperimentInstance from '../components/ExperimentInstance'; | |||||
| import { AutoMLData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function AutoMLList() { | |||||
| const navigate = useNavigate(); | |||||
| const { message } = App.useApp(); | |||||
| const [cacheState, setCacheState] = useCacheState(); | |||||
| const [searchText, setSearchText] = useState(cacheState?.searchText); | |||||
| const [inputText, setInputText] = useState(cacheState?.searchText); | |||||
| const [tableData, setTableData] = useState<AutoMLData[]>([]); | |||||
| const [total, setTotal] = useState(0); | |||||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||||
| cacheState?.pagination ?? { | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }, | |||||
| ); | |||||
| useEffect(() => { | |||||
| getAutoMLList(); | |||||
| }, [pagination, searchText]); | |||||
| // 获取自主机器学习列表 | |||||
| const getAutoMLList = async () => { | |||||
| const params: Record<string, any> = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| ml_name: searchText || undefined, | |||||
| }; | |||||
| const [res] = await to(getAutoMLListReq(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setTableData(content); | |||||
| setTotal(totalElements); | |||||
| } | |||||
| }; | |||||
| // 搜索 | |||||
| const onSearch: SearchProps['onSearch'] = (value) => { | |||||
| setSearchText(value); | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| }; | |||||
| // 删除模型部署 | |||||
| const deleteAutoML = async (record: AutoMLData) => { | |||||
| const [res] = await to(deleteAutoMLReq(record.id)); | |||||
| if (res) { | |||||
| message.success('删除成功'); | |||||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||||
| // 否则直接刷新这一页的数据 | |||||
| // 避免回到第一页 | |||||
| if (tableData.length > 1) { | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| } else { | |||||
| getAutoMLList(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 处理删除 | |||||
| const handleAutoMLDelete = (record: AutoMLData) => { | |||||
| modalConfirm({ | |||||
| title: '删除后,该实验将不可恢复', | |||||
| content: '是否确认删除?', | |||||
| onOk: () => { | |||||
| deleteAutoML(record); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 创建、编辑、复制自动机器学习 | |||||
| const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| if (record) { | |||||
| if (isCopy) { | |||||
| navigate(`/pipeline/autoML/copy/${record.id}`); | |||||
| } else { | |||||
| navigate(`/pipeline/autoML/edit/${record.id}`); | |||||
| } | |||||
| } else { | |||||
| navigate(`/pipeline/autoML/create`); | |||||
| } | |||||
| }; | |||||
| // 查看自动机器学习详情 | |||||
| const gotoDetail = (record: AutoMLData) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| navigate(`/pipeline/autoML/info/${record.id}`); | |||||
| }; | |||||
| // 启动自动机器学习 | |||||
| const startAutoML = async (record: AutoMLData) => { | |||||
| const [res] = await to(runAutoMLReq(record.id)); | |||||
| if (res) { | |||||
| message.success('运行成功'); | |||||
| setExpandedRowKeys([record.id]); | |||||
| refreshExperimentList(); | |||||
| refreshExperimentIns(record.id); | |||||
| } | |||||
| }; | |||||
| import ExperimentList, { ExperimentListType } from '../components/ExperimentList'; | |||||
| // --------------------------- 实验实例 --------------------------- | |||||
| // 获取实验实例列表 | |||||
| const getExperimentInsList = async (autoMLId: number, page: number) => { | |||||
| const params = { | |||||
| autoMlId: autoMLId, | |||||
| page: page, | |||||
| size: 5, | |||||
| }; | |||||
| const [res] = await to(getExperimentInsListReq(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| try { | |||||
| if (page === 0) { | |||||
| setExperimentInsList(content); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...content]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 展开实例 | |||||
| const handleExpandChange = (expanded: boolean, record: AutoMLData) => { | |||||
| setExperimentInsList([]); | |||||
| if (expanded) { | |||||
| setExpandedRowKeys([record.id]); | |||||
| getExperimentInsList(record.id, 0); | |||||
| } else { | |||||
| setExpandedRowKeys([]); | |||||
| } | |||||
| }; | |||||
| // 跳转到实验实例详情 | |||||
| const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | |||||
| navigate({ pathname: `/pipeline/automl/instance/${autoML.id}/${record.id}` }); | |||||
| }; | |||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = (experimentId: number) => { | |||||
| getExperimentInsList(experimentId, 0); | |||||
| }; | |||||
| // 加载更多实验实例 | |||||
| const loadMoreExperimentIns = () => { | |||||
| const page = Math.round(experimentInsList.length / 5); | |||||
| const autoMLId = expandedRowKeys[0]; | |||||
| getExperimentInsList(autoMLId, page); | |||||
| }; | |||||
| // 实验实例终止 | |||||
| const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | |||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | |||||
| if (item.id === experimentIns.id) { | |||||
| return { | |||||
| ...item, | |||||
| status: ExperimentStatus.Terminated, | |||||
| }; | |||||
| } | |||||
| return item; | |||||
| }); | |||||
| }); | |||||
| }; | |||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getAutoMLList(); | |||||
| }; | |||||
| // --------------------------- Table --------------------------- | |||||
| // 分页切换 | |||||
| const handleTableChange: TableProps<AutoMLData>['onChange'] = ( | |||||
| pagination, | |||||
| _filters, | |||||
| _sorter, | |||||
| { action }, | |||||
| ) => { | |||||
| if (action === 'paginate') { | |||||
| setPagination(pagination); | |||||
| } | |||||
| }; | |||||
| const columns: TableProps<AutoMLData>['columns'] = [ | |||||
| { | |||||
| title: '实验名称', | |||||
| dataIndex: 'ml_name', | |||||
| key: 'ml_name', | |||||
| width: '16%', | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: gotoDetail, | |||||
| }), | |||||
| }, | |||||
| { | |||||
| title: '实验描述', | |||||
| dataIndex: 'ml_description', | |||||
| key: 'ml_description', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '创建时间', | |||||
| dataIndex: 'update_time', | |||||
| key: 'update_time', | |||||
| width: '20%', | |||||
| render: tableCellRender(true, TableCellValueType.Date), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '最近五次运行状态', | |||||
| dataIndex: 'status_list', | |||||
| key: 'status_list', | |||||
| width: 200, | |||||
| render: (text) => { | |||||
| const newText: string[] = text && text.replace(/\s+/g, '').split(','); | |||||
| return ( | |||||
| <> | |||||
| {newText && newText.length > 0 | |||||
| ? newText.map((item, index) => { | |||||
| return ( | |||||
| <Tooltip | |||||
| key={index} | |||||
| placement="top" | |||||
| title={experimentStatusInfo[item as ExperimentStatus].label} | |||||
| > | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '6px' }} | |||||
| src={experimentStatusInfo[item as ExperimentStatus].icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| </Tooltip> | |||||
| ); | |||||
| }) | |||||
| : null} | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| title: '操作', | |||||
| dataIndex: 'operation', | |||||
| width: 360, | |||||
| key: 'operation', | |||||
| render: (_: any, record: AutoMLData) => ( | |||||
| <div> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="start" | |||||
| icon={<KFIcon type="icon-yunhang" />} | |||||
| onClick={() => startAutoML(record)} | |||||
| > | |||||
| 运行 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="edit" | |||||
| icon={<KFIcon type="icon-bianji" />} | |||||
| onClick={() => createAutoML(record, false)} | |||||
| > | |||||
| 编辑 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="copy" | |||||
| icon={<KFIcon type="icon-fuzhi" />} | |||||
| onClick={() => createAutoML(record, true)} | |||||
| > | |||||
| 复制 | |||||
| </Button> | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="remove" | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => handleAutoMLDelete(record)} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div className={styles['auto-ml-list']}> | |||||
| <PageTitle title="自动机器学习列表"></PageTitle> | |||||
| <div className={styles['auto-ml-list__content']}> | |||||
| <div className={styles['auto-ml-list__content__filter']}> | |||||
| <Input.Search | |||||
| placeholder="按实验名称筛选" | |||||
| onSearch={onSearch} | |||||
| onChange={(e) => setInputText(e.target.value)} | |||||
| style={{ width: 300 }} | |||||
| value={inputText} | |||||
| allowClear | |||||
| /> | |||||
| <Button | |||||
| style={{ marginLeft: '20px' }} | |||||
| type="default" | |||||
| onClick={() => createAutoML()} | |||||
| icon={<KFIcon type="icon-xinjian2" />} | |||||
| > | |||||
| 新建实验 | |||||
| </Button> | |||||
| </div> | |||||
| <div | |||||
| className={classNames('vertical-scroll-table', styles['auto-ml-list__content__table'])} | |||||
| > | |||||
| <Table | |||||
| dataSource={tableData} | |||||
| columns={columns} | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| pagination={{ | |||||
| ...pagination, | |||||
| total: total, | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| }} | |||||
| onChange={handleTableChange} | |||||
| expandable={{ | |||||
| expandedRowRender: (record) => ( | |||||
| <ExperimentInstance | |||||
| experimentInsList={experimentInsList} | |||||
| experimentInsTotal={experimentInsTotal} | |||||
| onClickInstance={(item) => gotoInstanceInfo(record, item)} | |||||
| onRemove={() => { | |||||
| refreshExperimentIns(record.id); | |||||
| refreshExperimentList(); | |||||
| }} | |||||
| onTerminate={handleInstanceTerminate} | |||||
| onLoadMore={() => loadMoreExperimentIns()} | |||||
| ></ExperimentInstance> | |||||
| ), | |||||
| onExpand: (e, a) => { | |||||
| handleExpandChange(e, a); | |||||
| }, | |||||
| expandedRowKeys: expandedRowKeys, | |||||
| rowExpandable: () => true, | |||||
| }} | |||||
| rowKey="id" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| function AutoMLList() { | |||||
| return <ExperimentList type={ExperimentListType.AutoML} />; | |||||
| } | } | ||||
| export default AutoMLList; | export default AutoMLList; | ||||
| @@ -62,10 +62,7 @@ function TrialConfig() { | |||||
| > | > | ||||
| <InputNumber placeholder="请输入指标权重" min={0} precision={0} /> | <InputNumber placeholder="请输入指标权重" min={0} precision={0} /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <Flex | |||||
| style={{ width: '76px', marginLeft: '18px', height: '46px' }} | |||||
| align="center" | |||||
| > | |||||
| <Flex className={styles['metrics-weight__operation']} align="center"> | |||||
| <Button | <Button | ||||
| style={{ | style={{ | ||||
| marginRight: '3px', | marginRight: '3px', | ||||
| @@ -1,9 +1,18 @@ | |||||
| .metrics-weight { | .metrics-weight { | ||||
| position: relative; | |||||
| margin-bottom: 20px; | margin-bottom: 20px; | ||||
| &:last-child { | &:last-child { | ||||
| margin-bottom: 0; | margin-bottom: 0; | ||||
| } | } | ||||
| &__operation { | |||||
| position: absolute; | |||||
| left: calc(100% + 10px); | |||||
| width: 76px; | |||||
| height: 46px; | |||||
| margin-left: 6px; | |||||
| } | |||||
| } | } | ||||
| .add-weight { | .add-weight { | ||||
| @@ -14,7 +23,7 @@ | |||||
| border-color: .addAlpha(@primary-color, 0.5) []; | border-color: .addAlpha(@primary-color, 0.5) []; | ||||
| box-shadow: none !important; | box-shadow: none !important; | ||||
| &:hover { | &:hover { | ||||
| border-style: solid; | |||||
| border-style: solid !important; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -2,11 +2,6 @@ import KFIcon from '@/components/KFIcon'; | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useCheck } from '@/hooks'; | import { useCheck } from '@/hooks'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { | |||||
| batchDeleteExperimentInsReq, | |||||
| deleteExperimentInsReq, | |||||
| stopExperimentInsReq, | |||||
| } from '@/services/autoML'; | |||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { type ExperimentInstance } from '@/types'; | import { type ExperimentInstance } from '@/types'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | import { elapsedTime, formatDate } from '@/utils/date'; | ||||
| @@ -16,9 +11,11 @@ import { DoubleRightOutlined } from '@ant-design/icons'; | |||||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useMemo } from 'react'; | import { useEffect, useMemo } from 'react'; | ||||
| import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentInstanceProps = { | type ExperimentInstanceProps = { | ||||
| type: ExperimentListType; | |||||
| experimentInsList?: ExperimentInstance[]; | experimentInsList?: ExperimentInstance[]; | ||||
| experimentInsTotal: number; | experimentInsTotal: number; | ||||
| onClickInstance?: (instance: ExperimentInstance) => void; | onClickInstance?: (instance: ExperimentInstance) => void; | ||||
| @@ -28,6 +25,7 @@ type ExperimentInstanceProps = { | |||||
| }; | }; | ||||
| function ExperimentInstanceComponent({ | function ExperimentInstanceComponent({ | ||||
| type, | |||||
| experimentInsList, | experimentInsList, | ||||
| experimentInsTotal, | experimentInsTotal, | ||||
| onClickInstance, | onClickInstance, | ||||
| @@ -48,6 +46,7 @@ function ExperimentInstanceComponent({ | |||||
| isSingleChecked, | isSingleChecked, | ||||
| checkSingle, | checkSingle, | ||||
| ] = useCheck(allIntanceIds); | ] = useCheck(allIntanceIds); | ||||
| const config = experimentListConfig[type]; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // 关闭时清空 | // 关闭时清空 | ||||
| @@ -68,7 +67,8 @@ function ExperimentInstanceComponent({ | |||||
| // 删除实验实例 | // 删除实验实例 | ||||
| const deleteExperimentInstance = async (id: number) => { | const deleteExperimentInstance = async (id: number) => { | ||||
| const [res] = await to(deleteExperimentInsReq(id)); | |||||
| const request = config.deleteInsReq; | |||||
| const [res] = await to(request(id)); | |||||
| if (res) { | if (res) { | ||||
| message.success('删除成功'); | message.success('删除成功'); | ||||
| onRemove?.(); | onRemove?.(); | ||||
| @@ -87,7 +87,8 @@ function ExperimentInstanceComponent({ | |||||
| // 批量删除实验实例 | // 批量删除实验实例 | ||||
| const batchDeleteExperimentInstances = async () => { | const batchDeleteExperimentInstances = async () => { | ||||
| const [res] = await to(batchDeleteExperimentInsReq(selectedIns)); | |||||
| const request = config.batchDeleteInsReq; | |||||
| const [res] = await to(request(selectedIns)); | |||||
| if (res) { | if (res) { | ||||
| message.success('删除成功'); | message.success('删除成功'); | ||||
| setSelectedIns([]); | setSelectedIns([]); | ||||
| @@ -97,7 +98,8 @@ function ExperimentInstanceComponent({ | |||||
| // 终止实验实例 | // 终止实验实例 | ||||
| const terminateExperimentInstance = async (instance: ExperimentInstance) => { | const terminateExperimentInstance = async (instance: ExperimentInstance) => { | ||||
| const [res] = await to(stopExperimentInsReq(instance.id)); | |||||
| const request = config.stopInsReq; | |||||
| const [res] = await to(request(instance.id)); | |||||
| if (res) { | if (res) { | ||||
| message.success('终止成功'); | message.success('终止成功'); | ||||
| onTerminate?.(instance); | onTerminate?.(instance); | ||||
| @@ -0,0 +1,75 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2025-01-08 14:30:58 | |||||
| * @Description: 实验列表组件配置 | |||||
| */ | |||||
| import { | |||||
| batchDeleteExperimentInsReq, | |||||
| deleteAutoMLReq, | |||||
| deleteExperimentInsReq, | |||||
| getAutoMLListReq, | |||||
| getExperimentInsListReq, | |||||
| runAutoMLReq, | |||||
| stopExperimentInsReq, | |||||
| } from '@/services/autoML'; | |||||
| import { | |||||
| batchDeleteRayInsReq, | |||||
| deleteRayInsReq, | |||||
| deleteRayReq, | |||||
| getRayInsListReq, | |||||
| getRayListReq, | |||||
| runRayReq, | |||||
| stopRayInsReq, | |||||
| } from '@/services/hyperParameter'; | |||||
| export enum ExperimentListType { | |||||
| AutoML = 'AutoML', | |||||
| HyperParameter = 'HyperParameter', | |||||
| } | |||||
| type ExperimentListInfo = { | |||||
| getListReq: (params: any) => Promise<any>; // 获取列表 | |||||
| getInsListReq: (params: any) => Promise<any>; // 获取实例列表 | |||||
| deleteRecordReq: (params: any) => Promise<any>; // 删除 | |||||
| runRecordReq: (params: any) => Promise<any>; // 运行 | |||||
| deleteInsReq: (params: any) => Promise<any>; // 删除实例 | |||||
| batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例 | |||||
| stopInsReq: (params: any) => Promise<any>; // 终止实例 | |||||
| title: string; // 标题 | |||||
| pathPrefix: string; // 路由路径前缀 | |||||
| idProperty: string; // ID属性 | |||||
| nameProperty: string; // 名称属性 | |||||
| descProperty: string; // 描述属性 | |||||
| }; | |||||
| export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = { | |||||
| [ExperimentListType.AutoML]: { | |||||
| getListReq: getAutoMLListReq, | |||||
| getInsListReq: getExperimentInsListReq, | |||||
| deleteRecordReq: deleteAutoMLReq, | |||||
| runRecordReq: runAutoMLReq, | |||||
| deleteInsReq: deleteExperimentInsReq, | |||||
| batchDeleteInsReq: batchDeleteExperimentInsReq, | |||||
| stopInsReq: stopExperimentInsReq, | |||||
| title: '自主机器学习', | |||||
| pathPrefix: 'automl', | |||||
| nameProperty: 'ml_name', | |||||
| descProperty: 'ml_description', | |||||
| idProperty: 'autoMlId', | |||||
| }, | |||||
| [ExperimentListType.HyperParameter]: { | |||||
| getListReq: getRayListReq, | |||||
| getInsListReq: getRayInsListReq, | |||||
| deleteRecordReq: deleteRayReq, | |||||
| runRecordReq: runRayReq, | |||||
| deleteInsReq: deleteRayInsReq, | |||||
| batchDeleteInsReq: batchDeleteRayInsReq, | |||||
| stopInsReq: stopRayInsReq, | |||||
| title: '超参数自动寻优', | |||||
| pathPrefix: 'hyperparameter', | |||||
| nameProperty: 'name', | |||||
| descProperty: 'description', | |||||
| idProperty: 'rayId', | |||||
| }, | |||||
| }; | |||||
| @@ -1,4 +1,4 @@ | |||||
| .auto-ml-list { | |||||
| .experiment-list { | |||||
| height: 100%; | height: 100%; | ||||
| &__content { | &__content { | ||||
| height: calc(100% - 60px); | height: calc(100% - 60px); | ||||
| @@ -0,0 +1,428 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2025-01-08 13:58:08 | |||||
| * @Description: 自主机器学习和超参数寻优列表组件 | |||||
| */ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||||
| import { AutoMLData } from '@/pages/AutoML/types'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import themes from '@/styles/theme.less'; | |||||
| import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { | |||||
| App, | |||||
| Button, | |||||
| ConfigProvider, | |||||
| Input, | |||||
| Table, | |||||
| Tooltip, | |||||
| type TablePaginationConfig, | |||||
| type TableProps, | |||||
| } from 'antd'; | |||||
| import { type SearchProps } from 'antd/es/input'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import ExperimentInstance from '../ExperimentInstance'; | |||||
| import { ExperimentListType, experimentListConfig } from './config'; | |||||
| import styles from './index.less'; | |||||
| export { ExperimentListType }; | |||||
| type ExperimentListProps = { | |||||
| type: ExperimentListType; | |||||
| }; | |||||
| function ExperimentList({ type }: ExperimentListProps) { | |||||
| const navigate = useNavigate(); | |||||
| const { message } = App.useApp(); | |||||
| const [cacheState, setCacheState] = useCacheState(); | |||||
| const [searchText, setSearchText] = useState(cacheState?.searchText); | |||||
| const [inputText, setInputText] = useState(cacheState?.searchText); | |||||
| const [tableData, setTableData] = useState<AutoMLData[]>([]); | |||||
| const [total, setTotal] = useState(0); | |||||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||||
| cacheState?.pagination ?? { | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }, | |||||
| ); | |||||
| const config = experimentListConfig[type]; | |||||
| useEffect(() => { | |||||
| getAutoMLList(); | |||||
| }, [pagination, searchText]); | |||||
| // 获取自主机器学习或超参数自动优化列表 | |||||
| const getAutoMLList = async () => { | |||||
| const params: Record<string, any> = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| ml_name: searchText || undefined, | |||||
| }; | |||||
| const request = config.getListReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setTableData(content); | |||||
| setTotal(totalElements); | |||||
| } | |||||
| }; | |||||
| // 搜索 | |||||
| const onSearch: SearchProps['onSearch'] = (value) => { | |||||
| setSearchText(value); | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| }; | |||||
| // 删除一条记录 | |||||
| const deleteAutoML = async (record: AutoMLData) => { | |||||
| const request = config.deleteRecordReq; | |||||
| const [res] = await to(request(record.id)); | |||||
| if (res) { | |||||
| message.success('删除成功'); | |||||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||||
| // 否则直接刷新这一页的数据 | |||||
| // 避免回到第一页 | |||||
| if (tableData.length > 1) { | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| } else { | |||||
| getAutoMLList(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 处理删除 | |||||
| const handleAutoMLDelete = (record: AutoMLData) => { | |||||
| modalConfirm({ | |||||
| title: '删除后,该实验将不可恢复', | |||||
| content: '是否确认删除?', | |||||
| onOk: () => { | |||||
| deleteAutoML(record); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 创建、编辑、复制自动机器学习 | |||||
| const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| if (record) { | |||||
| if (isCopy) { | |||||
| navigate(`copy/${record.id}`); | |||||
| } else { | |||||
| navigate(`edit/${record.id}`); | |||||
| } | |||||
| } else { | |||||
| navigate(`create`); | |||||
| } | |||||
| }; | |||||
| // 查看自动机器学习详情 | |||||
| const gotoDetail = (record: AutoMLData) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| navigate(`info/${record.id}`); | |||||
| }; | |||||
| // 启动自动机器学习 | |||||
| const startAutoML = async (record: AutoMLData) => { | |||||
| const request = config.runRecordReq; | |||||
| const [res] = await to(request(record.id)); | |||||
| if (res) { | |||||
| message.success('运行成功'); | |||||
| setExpandedRowKeys([record.id]); | |||||
| refreshExperimentList(); | |||||
| refreshExperimentIns(record.id); | |||||
| } | |||||
| }; | |||||
| // --------------------------- 实验实例 --------------------------- | |||||
| // 获取实验实例列表 | |||||
| const getExperimentInsList = async (recordId: number, page: number) => { | |||||
| const params = { | |||||
| [config.idProperty]: recordId, | |||||
| page: page, | |||||
| size: 5, | |||||
| }; | |||||
| const request = config.getInsListReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| try { | |||||
| if (page === 0) { | |||||
| setExperimentInsList(content); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...content]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 展开实例 | |||||
| const handleExpandChange = (expanded: boolean, record: AutoMLData) => { | |||||
| setExperimentInsList([]); | |||||
| if (expanded) { | |||||
| setExpandedRowKeys([record.id]); | |||||
| getExperimentInsList(record.id, 0); | |||||
| } else { | |||||
| setExpandedRowKeys([]); | |||||
| } | |||||
| }; | |||||
| // 跳转到实验实例详情 | |||||
| const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | |||||
| navigate(`instance/${autoML.id}/${record.id}`); | |||||
| }; | |||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = (experimentId: number) => { | |||||
| getExperimentInsList(experimentId, 0); | |||||
| }; | |||||
| // 加载更多实验实例 | |||||
| const loadMoreExperimentIns = () => { | |||||
| const page = Math.round(experimentInsList.length / 5); | |||||
| const recordId = expandedRowKeys[0]; | |||||
| getExperimentInsList(recordId, page); | |||||
| }; | |||||
| // 实验实例终止 | |||||
| const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | |||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | |||||
| if (item.id === experimentIns.id) { | |||||
| return { | |||||
| ...item, | |||||
| status: ExperimentStatus.Terminated, | |||||
| }; | |||||
| } | |||||
| return item; | |||||
| }); | |||||
| }); | |||||
| }; | |||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getAutoMLList(); | |||||
| }; | |||||
| // --------------------------- Table --------------------------- | |||||
| // 分页切换 | |||||
| const handleTableChange: TableProps<AutoMLData>['onChange'] = ( | |||||
| pagination, | |||||
| _filters, | |||||
| _sorter, | |||||
| { action }, | |||||
| ) => { | |||||
| if (action === 'paginate') { | |||||
| setPagination(pagination); | |||||
| } | |||||
| }; | |||||
| const columns: TableProps<AutoMLData>['columns'] = [ | |||||
| { | |||||
| title: '实验名称', | |||||
| dataIndex: config.nameProperty, | |||||
| key: 'ml_name', | |||||
| width: '16%', | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: gotoDetail, | |||||
| }), | |||||
| }, | |||||
| { | |||||
| title: '实验描述', | |||||
| dataIndex: config.descProperty, | |||||
| key: 'ml_description', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '创建时间', | |||||
| dataIndex: 'update_time', | |||||
| key: 'update_time', | |||||
| width: '20%', | |||||
| render: tableCellRender(true, TableCellValueType.Date), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '最近五次运行状态', | |||||
| dataIndex: 'status_list', | |||||
| key: 'status_list', | |||||
| width: 200, | |||||
| render: (text) => { | |||||
| const newText: string[] = text && text.replace(/\s+/g, '').split(','); | |||||
| return ( | |||||
| <> | |||||
| {newText && newText.length > 0 | |||||
| ? newText.map((item, index) => { | |||||
| return ( | |||||
| <Tooltip | |||||
| key={index} | |||||
| placement="top" | |||||
| title={experimentStatusInfo[item as ExperimentStatus].label} | |||||
| > | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '6px' }} | |||||
| src={experimentStatusInfo[item as ExperimentStatus].icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| </Tooltip> | |||||
| ); | |||||
| }) | |||||
| : null} | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| title: '操作', | |||||
| dataIndex: 'operation', | |||||
| width: 360, | |||||
| key: 'operation', | |||||
| render: (_: any, record: AutoMLData) => ( | |||||
| <div> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="start" | |||||
| icon={<KFIcon type="icon-yunhang" />} | |||||
| onClick={() => startAutoML(record)} | |||||
| > | |||||
| 运行 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="edit" | |||||
| icon={<KFIcon type="icon-bianji" />} | |||||
| onClick={() => createAutoML(record, false)} | |||||
| > | |||||
| 编辑 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="copy" | |||||
| icon={<KFIcon type="icon-fuzhi" />} | |||||
| onClick={() => createAutoML(record, true)} | |||||
| > | |||||
| 复制 | |||||
| </Button> | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="remove" | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => handleAutoMLDelete(record)} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div className={styles['experiment-list']}> | |||||
| <PageTitle title={config.title + '列表'}></PageTitle> | |||||
| <div className={styles['experiment-list__content']}> | |||||
| <div className={styles['experiment-list__content__filter']}> | |||||
| <Input.Search | |||||
| placeholder="按实验名称筛选" | |||||
| onSearch={onSearch} | |||||
| onChange={(e) => setInputText(e.target.value)} | |||||
| style={{ width: 300 }} | |||||
| value={inputText} | |||||
| allowClear | |||||
| /> | |||||
| <Button | |||||
| style={{ marginLeft: '20px' }} | |||||
| type="default" | |||||
| onClick={() => createAutoML()} | |||||
| icon={<KFIcon type="icon-xinjian2" />} | |||||
| > | |||||
| 新建实验 | |||||
| </Button> | |||||
| </div> | |||||
| <div | |||||
| className={classNames('vertical-scroll-table', styles['experiment-list__content__table'])} | |||||
| > | |||||
| <Table | |||||
| dataSource={tableData} | |||||
| columns={columns} | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| pagination={{ | |||||
| ...pagination, | |||||
| total: total, | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| }} | |||||
| onChange={handleTableChange} | |||||
| expandable={{ | |||||
| expandedRowRender: (record) => ( | |||||
| <ExperimentInstance | |||||
| experimentInsList={experimentInsList} | |||||
| experimentInsTotal={experimentInsTotal} | |||||
| onClickInstance={(item) => gotoInstanceInfo(record, item)} | |||||
| onRemove={() => { | |||||
| refreshExperimentIns(record.id); | |||||
| refreshExperimentList(); | |||||
| }} | |||||
| onTerminate={handleInstanceTerminate} | |||||
| onLoadMore={() => loadMoreExperimentIns()} | |||||
| ></ExperimentInstance> | |||||
| ), | |||||
| onExpand: (e, a) => { | |||||
| handleExpandChange(e, a); | |||||
| }, | |||||
| expandedRowKeys: expandedRowKeys, | |||||
| rowExpandable: () => true, | |||||
| }} | |||||
| rowKey="id" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentList; | |||||
| @@ -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: rgba(0, 0, 0, 0.25); | |||||
| } | |||||
| .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,165 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-16 13:58:08 | |||||
| * @Description: 创建实验 | |||||
| */ | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { addRayReq, getRayInfoReq, updateRayReq } from '@/services/hyperParameter'; | |||||
| import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useLocation, useNavigate, useParams } from '@umijs/max'; | |||||
| import { App, Button, Form } from 'antd'; | |||||
| import { useEffect } from 'react'; | |||||
| import BasicConfig from '../components/CreateForm/BasicConfig'; | |||||
| import ExecuteConfig from '../components/CreateForm/ExecuteConfig'; | |||||
| import { getReqParamName } from '../components/CreateForm/utils'; | |||||
| import { FormData, HyperparameterData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function CreateHyperparameter() { | |||||
| const navigate = useNavigate(); | |||||
| const [form] = Form.useForm(); | |||||
| const { message } = App.useApp(); | |||||
| const params = useParams(); | |||||
| const id = safeInvoke(Number)(params.id); | |||||
| const { pathname } = useLocation(); | |||||
| const isCopy = pathname.includes('copy'); | |||||
| useEffect(() => { | |||||
| // 编辑,复制 | |||||
| if (id && !Number.isNaN(id)) { | |||||
| getHyperparameterInfo(id); | |||||
| } | |||||
| }, [id]); | |||||
| // 获取服务详情 | |||||
| const getHyperparameterInfo = async (id: number) => { | |||||
| const [res] = await to(getRayInfoReq({ id })); | |||||
| if (res && res.data) { | |||||
| const info: HyperparameterData = res.data; | |||||
| const { name: name_str, parameters, points_to_evaluate, ...rest } = info; | |||||
| const name = isCopy ? `${name_str}-copy` : name_str; | |||||
| if (parameters && Array.isArray(parameters)) { | |||||
| parameters.forEach((item) => { | |||||
| item.range = item.bounds || item.values || item.value; | |||||
| delete item.bounds; | |||||
| delete item.values; | |||||
| delete item.value; | |||||
| }); | |||||
| } | |||||
| const formData = { | |||||
| ...rest, | |||||
| name, | |||||
| parameters, | |||||
| points_to_evaluate: points_to_evaluate ?? [undefined], | |||||
| }; | |||||
| form.setFieldsValue(formData); | |||||
| } | |||||
| }; | |||||
| // 创建、更新、复制实验 | |||||
| const createExperiment = async (formData: FormData) => { | |||||
| // 按后台接口要求,修改参数表单数据结构,将 "value" 参数改为 "bounds"/"values"/"value" | |||||
| const parameters = formData['parameters']; | |||||
| // const points_to_evaluate = formData['points_to_evaluate']; | |||||
| // const runParameters = formData['parameters']; | |||||
| parameters.forEach((item) => { | |||||
| const paramName = getReqParamName(item.type); | |||||
| item[paramName] = item.range; | |||||
| delete item.range; | |||||
| }); | |||||
| // 根据后台要求,修改表单数据 | |||||
| const object = { | |||||
| ...formData, | |||||
| parameters: parameters, | |||||
| }; | |||||
| const params = | |||||
| id && !isCopy | |||||
| ? { | |||||
| id: id, | |||||
| ...object, | |||||
| } | |||||
| : object; | |||||
| const request = id && !isCopy ? updateRayReq : addRayReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res) { | |||||
| message.success('操作成功'); | |||||
| navigate(-1); | |||||
| } | |||||
| }; | |||||
| // 提交 | |||||
| const handleSubmit = (values: FormData) => { | |||||
| createExperiment(values); | |||||
| }; | |||||
| // 取消 | |||||
| const cancel = () => { | |||||
| navigate(-1); | |||||
| }; | |||||
| let buttonText = '新建'; | |||||
| let title = '新建实验'; | |||||
| if (id) { | |||||
| if (isCopy) { | |||||
| title = '复制实验'; | |||||
| buttonText = '确定'; | |||||
| } else { | |||||
| title = '编辑实验'; | |||||
| buttonText = '更新'; | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <div className={styles['create-hyperparameter']}> | |||||
| <PageTitle title={title}></PageTitle> | |||||
| <div className={styles['create-hyperparameter__content']}> | |||||
| <div> | |||||
| <Form | |||||
| name="create-hyperparameter" | |||||
| labelCol={{ flex: '160px' }} | |||||
| labelAlign="left" | |||||
| form={form} | |||||
| onFinish={handleSubmit} | |||||
| size="large" | |||||
| autoComplete="off" | |||||
| scrollToFirstError | |||||
| initialValues={{ | |||||
| mode: 'max', | |||||
| parameters: [ | |||||
| { | |||||
| name: '', | |||||
| }, | |||||
| ], | |||||
| points_to_evaluate: [undefined], | |||||
| }} | |||||
| > | |||||
| <BasicConfig /> | |||||
| <ExecuteConfig /> | |||||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||||
| <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 CreateHyperparameter; | |||||
| @@ -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,61 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-16 13:58:08 | |||||
| * @Description: 自主机器学习详情 | |||||
| */ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { CommonTabKeys } from '@/enums'; | |||||
| import { getAutoMLInfoReq } from '@/services/autoML'; | |||||
| import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useParams } from '@umijs/max'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import AutoMLBasic from '../components/AutoMLBasic'; | |||||
| import { HyperparameterData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function AutoMLInfo() { | |||||
| const [activeTab, setActiveTab] = useState<string>(CommonTabKeys.Public); | |||||
| const params = useParams(); | |||||
| const autoMLId = safeInvoke(Number)(params.id); | |||||
| const [autoMLInfo, setAutoMLInfo] = useState<HyperparameterData | undefined>(undefined); | |||||
| const tabItems = [ | |||||
| { | |||||
| key: CommonTabKeys.Public, | |||||
| label: '基本信息', | |||||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||||
| }, | |||||
| { | |||||
| key: CommonTabKeys.Private, | |||||
| label: 'Trial列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||||
| }, | |||||
| ]; | |||||
| useEffect(() => { | |||||
| if (autoMLId) { | |||||
| getAutoMLInfo(); | |||||
| } | |||||
| }, []); | |||||
| // 获取自动机器学习详情 | |||||
| const getAutoMLInfo = async () => { | |||||
| const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); | |||||
| if (res && res.data) { | |||||
| setAutoMLInfo(res.data); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <div className={styles['auto-ml-info']}> | |||||
| <PageTitle title="实验详情"></PageTitle> | |||||
| <div className={styles['auto-ml-info__content']}> | |||||
| <AutoMLBasic info={autoMLInfo} /> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default AutoMLInfo; | |||||
| @@ -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 AutoMLBasic from '../components/AutoMLBasic'; | |||||
| 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: ( | |||||
| <AutoMLBasic | |||||
| 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 HyperParameter() { | |||||
| return <ExperimentList type={ExperimentListType.HyperParameter} />; | |||||
| } | |||||
| export default HyperParameter; | |||||
| @@ -0,0 +1,13 @@ | |||||
| .auto-ml-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,308 @@ | |||||
| import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; | |||||
| import { AutoMLData } from '@/pages/AutoML/types'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { type NodeStatus } from '@/types'; | |||||
| import { parseJsonText } from '@/utils'; | |||||
| import { elapsedTime } from '@/utils/date'; | |||||
| import { Flex } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { useMemo } from 'react'; | |||||
| import ConfigInfo, { | |||||
| formatBoolean, | |||||
| formatDate, | |||||
| formatEnum, | |||||
| type BasicInfoData, | |||||
| } from '../ConfigInfo'; | |||||
| import styles from './index.less'; | |||||
| // 格式化数据集 | |||||
| const formatDataset = (dataset: { name: string; version: string }) => { | |||||
| if (!dataset || !dataset.name || !dataset.version) { | |||||
| return '--'; | |||||
| } | |||||
| return `${dataset.name}:${dataset.version}`; | |||||
| }; | |||||
| // 格式化优化方向 | |||||
| const formatOptimizeMode = (value: boolean) => { | |||||
| return value ? '越大越好' : '越小越好'; | |||||
| }; | |||||
| const formatMetricsWeight = (value: string) => { | |||||
| if (!value) { | |||||
| return '--'; | |||||
| } | |||||
| const json = parseJsonText(value); | |||||
| if (!json) { | |||||
| return '--'; | |||||
| } | |||||
| return Object.entries(json) | |||||
| .map(([key, value]) => `${key}:${value}`) | |||||
| .join('\n'); | |||||
| }; | |||||
| type AutoMLBasicProps = { | |||||
| info?: AutoMLData; | |||||
| className?: string; | |||||
| isInstance?: boolean; | |||||
| runStatus?: NodeStatus; | |||||
| }; | |||||
| function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLBasicProps) { | |||||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||||
| if (!info) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| label: '实验名称', | |||||
| value: info.ml_name, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '实验描述', | |||||
| value: info.ml_description, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '创建人', | |||||
| value: info.create_by, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '创建时间', | |||||
| value: info.create_time, | |||||
| ellipsis: true, | |||||
| format: formatDate, | |||||
| }, | |||||
| { | |||||
| label: '更新时间', | |||||
| value: info.update_time, | |||||
| ellipsis: true, | |||||
| format: formatDate, | |||||
| }, | |||||
| ]; | |||||
| }, [info]); | |||||
| const configDatas: BasicInfoData[] = useMemo(() => { | |||||
| if (!info) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| label: '任务类型', | |||||
| value: info.task_type, | |||||
| ellipsis: true, | |||||
| format: formatEnum(autoMLTaskTypeOptions), | |||||
| }, | |||||
| { | |||||
| label: '特征预处理算法', | |||||
| value: info.include_feature_preprocessor, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '排除的特征预处理算法', | |||||
| value: info.exclude_feature_preprocessor, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', | |||||
| value: | |||||
| info.task_type === AutoMLTaskType.Regression | |||||
| ? info.include_regressor | |||||
| : info.include_classifier, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', | |||||
| value: | |||||
| info.task_type === AutoMLTaskType.Regression | |||||
| ? info.exclude_regressor | |||||
| : info.exclude_classifier, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '集成方式', | |||||
| value: info.ensemble_class, | |||||
| ellipsis: true, | |||||
| format: formatEnum(autoMLEnsembleClassOptions), | |||||
| }, | |||||
| { | |||||
| label: '集成模型数量', | |||||
| value: info.ensemble_size, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '集成最佳模型数量', | |||||
| value: info.ensemble_nbest, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '最大数量', | |||||
| value: info.max_models_on_disc, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '内存限制(MB)', | |||||
| value: info.memory_limit, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '单次时间限制(秒)', | |||||
| value: info.per_run_time_limit, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '搜索时间限制(秒)', | |||||
| value: info.time_left_for_this_task, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '重采样策略', | |||||
| value: info.resampling_strategy, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '交叉验证折数', | |||||
| value: info.folds, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '是否打乱', | |||||
| value: info.shuffle, | |||||
| ellipsis: true, | |||||
| format: formatBoolean, | |||||
| }, | |||||
| { | |||||
| label: '训练集比率', | |||||
| value: info.train_size, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '测试集比率', | |||||
| value: info.test_size, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '计算指标', | |||||
| value: info.scoring_functions, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '随机种子', | |||||
| value: info.seed, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '数据集', | |||||
| value: info.dataset, | |||||
| ellipsis: true, | |||||
| format: formatDataset, | |||||
| }, | |||||
| { | |||||
| label: '预测目标列', | |||||
| value: info.target_columns, | |||||
| ellipsis: true, | |||||
| }, | |||||
| ]; | |||||
| }, [info]); | |||||
| const metricsData = useMemo(() => { | |||||
| if (!info) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| label: '指标名称', | |||||
| value: info.metric_name, | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '优化方向', | |||||
| value: info.greater_is_better, | |||||
| ellipsis: true, | |||||
| format: formatOptimizeMode, | |||||
| }, | |||||
| { | |||||
| label: '指标权重', | |||||
| value: info.metrics, | |||||
| ellipsis: true, | |||||
| format: formatMetricsWeight, | |||||
| }, | |||||
| ]; | |||||
| }, [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['auto-ml-basic'], className)}> | |||||
| {isInstance && runStatus && ( | |||||
| <ConfigInfo | |||||
| title="运行信息" | |||||
| data={instanceDatas} | |||||
| labelWidth={70} | |||||
| style={{ marginBottom: '20px' }} | |||||
| /> | |||||
| )} | |||||
| {!isInstance && ( | |||||
| <ConfigInfo | |||||
| title="基本信息" | |||||
| data={basicDatas} | |||||
| labelWidth={70} | |||||
| style={{ marginBottom: '20px' }} | |||||
| /> | |||||
| )} | |||||
| <ConfigInfo | |||||
| title="配置信息" | |||||
| data={configDatas} | |||||
| labelWidth={150} | |||||
| style={{ marginBottom: '20px' }} | |||||
| /> | |||||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default AutoMLBasic; | |||||
| @@ -0,0 +1,20 @@ | |||||
| .config-info { | |||||
| :global { | |||||
| .kf-basic-info { | |||||
| width: 100%; | |||||
| &__item { | |||||
| width: calc((100% - 80px) / 3); | |||||
| &__label { | |||||
| font-size: @font-size; | |||||
| text-align: left; | |||||
| text-align-last: left; | |||||
| } | |||||
| &__value { | |||||
| min-width: 0; | |||||
| font-size: @font-size; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import classNames from 'classnames'; | |||||
| import styles from './index.less'; | |||||
| export * from '@/components/BasicInfo/format'; | |||||
| export type { BasicInfoData }; | |||||
| type ConfigInfoProps = { | |||||
| title: string; | |||||
| data: BasicInfoData[]; | |||||
| labelWidth: number; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| }; | |||||
| function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) { | |||||
| return ( | |||||
| <InfoGroup title={title} className={classNames(styles['config-info'], className)} style={style}> | |||||
| <div className={styles['config-info__content']}> | |||||
| <BasicInfo datas={data} labelWidth={labelWidth} /> | |||||
| </div> | |||||
| </InfoGroup> | |||||
| ); | |||||
| } | |||||
| export default ConfigInfo; | |||||
| @@ -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,504 @@ | |||||
| import CodeSelect from '@/components/CodeSelect'; | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import ResourceSelect, { | |||||
| ResourceSelectorType, | |||||
| requiredValidator, | |||||
| } from '@/components/ResourceSelect'; | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; | |||||
| import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; | |||||
| import { isEqual } from 'lodash'; | |||||
| import PopParameterRange from './PopParameterRange'; | |||||
| import styles from './index.less'; | |||||
| import { axParameterOptions, parameterOptions, type FormParameter } from './utils'; | |||||
| // 搜索算法 | |||||
| const searchAlgorithms = ['HyperOpt', 'HEBO', 'BayesOpt', 'Optuna', 'ZOOpt', 'Ax'].map((name) => ({ | |||||
| label: name, | |||||
| value: name, | |||||
| })); | |||||
| // 调度算法 | |||||
| const schedulerAlgorithms = ['ASHA', 'HyperBand', 'MedianStopping', 'PopulationBased', 'PB2'].map( | |||||
| (name) => ({ label: name, value: name }), | |||||
| ); | |||||
| function ExecuteConfig() { | |||||
| const form = Form.useFormInstance(); | |||||
| const searchAlgorithm = Form.useWatch('search_alg', form); | |||||
| const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions; | |||||
| // const parameters = Form.useWatch('parameters', form); | |||||
| // console.log('parameters', parameters); | |||||
| const handleSearchAlgorithmChange = (value: string) => { | |||||
| if ( | |||||
| (value === 'Ax' && searchAlgorithm !== 'Ax') || | |||||
| (value !== 'Ax' && searchAlgorithm === 'Ax') | |||||
| ) { | |||||
| form.setFieldValue('parameters', [{ name: '' }]); | |||||
| } | |||||
| }; | |||||
| 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="code" | |||||
| rules={[ | |||||
| { | |||||
| validator: requiredValidator, | |||||
| message: '请选择代码配置', | |||||
| }, | |||||
| ]} | |||||
| required | |||||
| > | |||||
| <CodeSelect placeholder="请选择代码配置" canInput={false} size="large" /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="主函数代码文件" | |||||
| name="main_py" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入主函数代码文件', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入主函数代码文件" maxLength={64} showCount 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="dataset_path" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入数据集挂载路径', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入数据集挂载路径" maxLength={64} showCount allowClear /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="总实验次数" | |||||
| name="num_samples" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入总实验次数', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入总实验次数" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="搜索算法" name="search_alg"> | |||||
| <Select | |||||
| placeholder="请选择搜索算法" | |||||
| options={searchAlgorithms} | |||||
| showSearch | |||||
| allowClear | |||||
| onChange={handleSearchAlgorithmChange} | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="调度算法" name="scheduler"> | |||||
| <Select | |||||
| placeholder="请选择调度算法" | |||||
| options={schedulerAlgorithms} | |||||
| showSearch | |||||
| allowClear | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Form.Item dependencies={['scheduler']} noStyle> | |||||
| {({ getFieldValue }) => { | |||||
| const schedulerAlgorithm = getFieldValue('scheduler'); | |||||
| if (schedulerAlgorithm === 'ASHA' || schedulerAlgorithm === 'HyperBand') { | |||||
| return ( | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="单次试验最大时间" | |||||
| name="max_t" | |||||
| tooltip="每次试验的最大时间单位,单位秒" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '单次试验最大时间', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入单次试验最大时间" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| ); | |||||
| } else if (schedulerAlgorithm === 'MedianStopping') { | |||||
| return ( | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="最小试验数" | |||||
| name="min_samples_required" | |||||
| tooltip="计算中位数的最小试验数" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入计算中位数的最小试验数', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入计算中位数的最小试验数" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| ); | |||||
| } | |||||
| return null; | |||||
| }} | |||||
| </Form.Item> | |||||
| <Form.List name="parameters"> | |||||
| {(fields, { add, remove }) => ( | |||||
| <> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="参数" | |||||
| style={{ marginBottom: 0, marginTop: '-14px' }} | |||||
| required | |||||
| ></Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <div className={styles['hyper-parameter']}> | |||||
| <Flex align="center" className={styles['hyper-parameter__header']}> | |||||
| <div className={styles['hyper-parameter__header__name']}>参数名称</div> | |||||
| <div className={styles['hyper-parameter__header__type']}>参数类型</div> | |||||
| <div className={styles['hyper-parameter__header__space']}>取值范围</div> | |||||
| <div className={styles['hyper-parameter__header__operation']}>操作</div> | |||||
| </Flex> | |||||
| {fields.map(({ key, name, ...restField }, index) => ( | |||||
| <Flex key={key} align="flex-start" className={styles['hyper-parameter__body']}> | |||||
| <Form.Item | |||||
| className={styles['hyper-parameter__body__name']} | |||||
| {...restField} | |||||
| name={[name, 'name']} | |||||
| required | |||||
| rules={[ | |||||
| { | |||||
| validator: (_, value) => { | |||||
| if (!value) { | |||||
| return Promise.reject(new Error('请输入参数名称')); | |||||
| } | |||||
| // 判断不能重名 | |||||
| const list = form | |||||
| .getFieldValue('parameters') | |||||
| .filter( | |||||
| (item: FormParameter | undefined) => | |||||
| item !== undefined && item !== null, | |||||
| ); | |||||
| const names = list.map((item: FormParameter) => item.name); | |||||
| if (new Set(names).size !== names.length) { | |||||
| return Promise.reject(new Error('名称不能重复')); | |||||
| } | |||||
| return Promise.resolve(); | |||||
| }, | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入参数名称" maxLength={64} showCount allowClear /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| className={styles['hyper-parameter__body__name']} | |||||
| {...restField} | |||||
| name={[name, 'type']} | |||||
| rules={[{ required: true, message: '请选择参数类型' }]} | |||||
| > | |||||
| <Select | |||||
| placeholder="请选择参数类型" | |||||
| options={paramsTypeOptions} | |||||
| onChange={() => { | |||||
| form.setFieldValue(['parameters', name, 'range'], undefined); | |||||
| }} | |||||
| showSearch | |||||
| allowClear | |||||
| /> | |||||
| </Form.Item> | |||||
| <Form.Item dependencies={[['parameters', name, 'type']]} noStyle> | |||||
| {({ getFieldValue }) => { | |||||
| const type = getFieldValue(['parameters', name, 'type']); | |||||
| return ( | |||||
| <Form.Item | |||||
| className={styles['hyper-parameter__body__name']} | |||||
| {...restField} | |||||
| name={[name, 'range']} | |||||
| rules={[{ required: true, message: '请输入取值范围' }]} | |||||
| > | |||||
| <PopParameterRange type={type}></PopParameterRange> | |||||
| </Form.Item> | |||||
| ); | |||||
| }} | |||||
| </Form.Item> | |||||
| <div className={styles['hyper-parameter__body__operation']}> | |||||
| <Button | |||||
| style={{ | |||||
| marginRight: '3px', | |||||
| }} | |||||
| shape="circle" | |||||
| disabled={fields.length === 1} | |||||
| type="text" | |||||
| size="middle" | |||||
| icon={<MinusCircleOutlined />} | |||||
| onClick={() => { | |||||
| modalConfirm({ | |||||
| title: '确定要删除该参数吗?', | |||||
| onOk: () => { | |||||
| remove(name); | |||||
| }, | |||||
| }); | |||||
| }} | |||||
| ></Button> | |||||
| {index === fields.length - 1 && ( | |||||
| <Button | |||||
| shape="circle" | |||||
| size="middle" | |||||
| type="text" | |||||
| onClick={() => add()} | |||||
| icon={<PlusCircleOutlined />} | |||||
| ></Button> | |||||
| )} | |||||
| </div> | |||||
| </Flex> | |||||
| ))} | |||||
| {fields.length === 0 && ( | |||||
| <div className={styles['hyper-parameter__add']}> | |||||
| <Button type="link" onClick={() => add()} icon={<KFIcon type="icon-xinjian2" />}> | |||||
| 添加一行 | |||||
| </Button> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| </> | |||||
| )} | |||||
| </Form.List> | |||||
| <Form.Item | |||||
| noStyle | |||||
| shouldUpdate={(prevValues, curValues) => | |||||
| !isEqual(prevValues.parameters, curValues.parameters) | |||||
| } | |||||
| > | |||||
| {({ getFieldValue }) => { | |||||
| const parameters = getFieldValue('parameters').filter( | |||||
| (item: FormParameter | undefined) => item !== undefined && item !== null && item.name, | |||||
| ); | |||||
| if (parameters.length === 0) { | |||||
| return null; | |||||
| } | |||||
| return ( | |||||
| <Form.List name="points_to_evaluate"> | |||||
| {(fields, { add, remove }) => ( | |||||
| <> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="手动运行参数" | |||||
| style={{ marginBottom: 0, marginTop: '-14px' }} | |||||
| required | |||||
| ></Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <div className={styles['run-parameter']}> | |||||
| {fields.map(({ key, name, ...restField }, index) => ( | |||||
| <Flex key={key} align="center" style={{ marginBottom: '20px' }}> | |||||
| <div className={styles['run-parameter__body']}> | |||||
| {parameters.map((item: FormParameter) => ( | |||||
| <Form.Item | |||||
| key={item.name} | |||||
| label={item.name} | |||||
| {...restField} | |||||
| labelCol={{ flex: '140px' }} | |||||
| name={[name, item.name]} | |||||
| preserve={false} | |||||
| required | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入" maxLength={64} showCount allowClear /> | |||||
| </Form.Item> | |||||
| ))} | |||||
| </div> | |||||
| <div className={styles['run-parameter__operation']}> | |||||
| <Button | |||||
| style={{ | |||||
| marginRight: '3px', | |||||
| }} | |||||
| shape="circle" | |||||
| disabled={fields.length === 1} | |||||
| type="text" | |||||
| size="middle" | |||||
| icon={<MinusCircleOutlined />} | |||||
| onClick={() => { | |||||
| modalConfirm({ | |||||
| title: '确定要删除该运行参数吗?', | |||||
| onOk: () => { | |||||
| remove(name); | |||||
| }, | |||||
| }); | |||||
| }} | |||||
| ></Button> | |||||
| {index === fields.length - 1 && ( | |||||
| <Button | |||||
| shape="circle" | |||||
| size="middle" | |||||
| type="text" | |||||
| onClick={() => add()} | |||||
| icon={<PlusCircleOutlined />} | |||||
| ></Button> | |||||
| )} | |||||
| </div> | |||||
| </Flex> | |||||
| ))} | |||||
| </div> | |||||
| </> | |||||
| )} | |||||
| </Form.List> | |||||
| ); | |||||
| }} | |||||
| </Form.Item> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="指标" | |||||
| name="metric" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入指标内容', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入指标内容" maxLength={64} showCount allowClear /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={0}> | |||||
| <Col span={24}> | |||||
| <Form.Item | |||||
| label="优化方向" | |||||
| name="mode" | |||||
| rules={[{ required: true, message: '请选择优化方向' }]} | |||||
| > | |||||
| <Radio.Group> | |||||
| <Radio value={'max'}>越大越好</Radio> | |||||
| <Radio value={'min'}>越小越好</Radio> | |||||
| </Radio.Group> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="CPU 数" | |||||
| name="cpu" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入 CPU 数', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入 CPU 数" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="GPU 数" | |||||
| name="gpu" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入 GPU 数', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入 GPU 数" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default ExecuteConfig; | |||||
| @@ -0,0 +1,13 @@ | |||||
| .parameter-range { | |||||
| width: 300px; | |||||
| &__list { | |||||
| width: 100%; | |||||
| max-height: 300px; | |||||
| overflow-x: visible; | |||||
| overflow-y: auto; | |||||
| } | |||||
| &__button { | |||||
| margin-bottom: 0; | |||||
| text-align: center; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,141 @@ | |||||
| import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; | |||||
| import { Button, Flex, Form, Input, InputNumber } from 'antd'; | |||||
| import { ParameterType, getFormOptions } from '../utils'; | |||||
| import styles from './index.less'; | |||||
| type ParameterRangeProps = { | |||||
| type?: ParameterType; | |||||
| value?: any[]; | |||||
| onCancel?: () => void; | |||||
| onConfirm?: (value: any[]) => void; | |||||
| }; | |||||
| function ParameterRange({ type, value, onCancel, onConfirm }: ParameterRangeProps) { | |||||
| const [form] = Form.useForm(); | |||||
| const isList = type === ParameterType.Choice || type === ParameterType.Grid; | |||||
| const formOptions = getFormOptions(type, value); | |||||
| const initialValues = isList | |||||
| ? { list: value && value.length > 0 ? value.map((item) => ({ value: item })) : [{ value: '' }] } | |||||
| : formOptions.reduce((prev, item) => { | |||||
| prev[item.name] = item.value; | |||||
| return prev; | |||||
| }, {} as Record<string, any>); | |||||
| const handleFinish = (values: any) => { | |||||
| if (type === ParameterType.Choice || type === ParameterType.Grid) { | |||||
| const array = values.list.map((item: any) => item.value); | |||||
| onConfirm?.(array); | |||||
| } else { | |||||
| const numbers = Object.values(values).map((item: any) => Number(item)); | |||||
| onConfirm?.(numbers); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <Form | |||||
| labelCol={{ flex: '70px' }} | |||||
| wrapperCol={{ flex: '1' }} | |||||
| labelAlign="left" | |||||
| form={form} | |||||
| onFinish={handleFinish} | |||||
| size="middle" | |||||
| autoComplete="off" | |||||
| scrollToFirstError | |||||
| initialValues={initialValues} | |||||
| className={styles['parameter-range']} | |||||
| > | |||||
| {isList ? ( | |||||
| <div className={styles['parameter-range__list']}> | |||||
| <Form.List name="list"> | |||||
| {(fields, { add, remove }) => ( | |||||
| <> | |||||
| {fields.map(({ key, name, ...restField }, index) => ( | |||||
| <Flex key={key} align="center"> | |||||
| <Form.Item | |||||
| style={{ flex: 1, minWidth: 0 }} | |||||
| {...restField} | |||||
| name={[name, 'value']} | |||||
| rules={[{ required: true, message: '必填' }]} | |||||
| > | |||||
| <Input placeholder="请输入" allowClear /> | |||||
| </Form.Item> | |||||
| <Flex | |||||
| style={{ | |||||
| marginLeft: '10px', | |||||
| marginBottom: '20px', | |||||
| flex: 'none', | |||||
| width: '66px', | |||||
| }} | |||||
| align="center" | |||||
| > | |||||
| <Button | |||||
| shape="circle" | |||||
| size="middle" | |||||
| type="text" | |||||
| disabled={fields.length === 1} | |||||
| icon={<MinusCircleOutlined />} | |||||
| onClick={() => remove(name)} | |||||
| ></Button> | |||||
| {index === fields.length - 1 && ( | |||||
| <Button | |||||
| shape="circle" | |||||
| size="middle" | |||||
| type="text" | |||||
| onClick={() => add()} | |||||
| icon={<PlusCircleOutlined />} | |||||
| ></Button> | |||||
| )} | |||||
| </Flex> | |||||
| </Flex> | |||||
| ))} | |||||
| {fields.length === 0 && ( | |||||
| <Form.Item className={styles['add-weight']}> | |||||
| <Button | |||||
| className={styles['add-weight__button']} | |||||
| color="primary" | |||||
| variant="dashed" | |||||
| onClick={() => add()} | |||||
| block | |||||
| icon={<PlusCircleOutlined />} | |||||
| > | |||||
| 添加 | |||||
| </Button> | |||||
| </Form.Item> | |||||
| )} | |||||
| </> | |||||
| )} | |||||
| </Form.List> | |||||
| </div> | |||||
| ) : ( | |||||
| formOptions.map((item) => { | |||||
| return ( | |||||
| <Form.Item | |||||
| key={item.name} | |||||
| label={item.label} | |||||
| name={item.name} | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: `请输入${item.label}`, | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber style={{ width: '100%' }} placeholder={`请输入${item.label}`} /> | |||||
| </Form.Item> | |||||
| ); | |||||
| }) | |||||
| )} | |||||
| <Form.Item className={styles['parameter-range__button']}> | |||||
| <Button type="default" htmlType="button" onClick={onCancel}> | |||||
| 取消 | |||||
| </Button> | |||||
| <Button type="primary" htmlType="submit" style={{ marginLeft: '20px' }}> | |||||
| 确定 | |||||
| </Button> | |||||
| </Form.Item> | |||||
| </Form> | |||||
| ); | |||||
| } | |||||
| export default ParameterRange; | |||||
| @@ -0,0 +1,47 @@ | |||||
| .parameter-range { | |||||
| :global { | |||||
| .ant-popconfirm-description { | |||||
| padding-top: 20px; | |||||
| } | |||||
| .ant-popconfirm-buttons { | |||||
| display: none; | |||||
| } | |||||
| } | |||||
| &__input { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| width: 100%; | |||||
| min-height: 46px; | |||||
| padding: 10px 11px; | |||||
| font-size: @font-size-input-lg; | |||||
| line-height: 1.5; | |||||
| background-color: white; | |||||
| border: 1px solid #d9d9d9; | |||||
| border-radius: 8px; | |||||
| cursor: pointer; | |||||
| &:hover { | |||||
| border-color: #4086ff; | |||||
| } | |||||
| &--disabled { | |||||
| background-color: rgba(0, 0, 0, 0.04); | |||||
| cursor: not-allowed; | |||||
| } | |||||
| &__text { | |||||
| flex: 1; | |||||
| margin-right: 10px; | |||||
| } | |||||
| &__icon { | |||||
| flex: none; | |||||
| } | |||||
| &--disabled &__icon { | |||||
| color: @text-color-tertiary; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,97 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { Popconfirm, Typography } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect, useRef, useState } from 'react'; | |||||
| import ParameterRange from '../ParameterRange'; | |||||
| import { ParameterType } from '../utils'; | |||||
| import styles from './index.less'; | |||||
| type ParameterRangeProps = { | |||||
| type?: ParameterType; | |||||
| value?: any[]; | |||||
| onChange?: (value: any[]) => void; | |||||
| }; | |||||
| function PopParameterRange({ type, value, onChange }: ParameterRangeProps) { | |||||
| const [open, setOpen] = useState(false); | |||||
| const popconfirmRef = useRef<HTMLDivElement | null>(null); | |||||
| const disabled = !type; | |||||
| const jsonText = JSON.stringify(value); | |||||
| const handleClickOutside = (event: MouseEvent) => { | |||||
| // 判断点击是否在 Popconfirm 内 | |||||
| const popconfirmNode = document.getElementById('pop-parameter'); | |||||
| if (popconfirmNode && !popconfirmNode.contains(event.target as Node)) { | |||||
| setOpen(false); | |||||
| } | |||||
| }; | |||||
| useEffect(() => { | |||||
| if (open) { | |||||
| document.addEventListener('mousedown', handleClickOutside); | |||||
| } else { | |||||
| document.removeEventListener('mousedown', handleClickOutside); | |||||
| } | |||||
| // 清理事件监听器 | |||||
| return () => { | |||||
| document.removeEventListener('mousedown', handleClickOutside); | |||||
| }; | |||||
| }, [open]); | |||||
| const handleClick = () => { | |||||
| if (!disabled) { | |||||
| setOpen(true); | |||||
| } | |||||
| }; | |||||
| const handleCancel = () => { | |||||
| setOpen(false); | |||||
| }; | |||||
| const handleConfirm = (value: number[]) => { | |||||
| onChange?.(value); | |||||
| setOpen(false); | |||||
| }; | |||||
| return ( | |||||
| <div ref={popconfirmRef}> | |||||
| <Popconfirm | |||||
| id="pop-parameter" | |||||
| title="参数范围" | |||||
| disabled={disabled} | |||||
| description={ | |||||
| <ParameterRange | |||||
| type={type} | |||||
| value={value} | |||||
| onCancel={handleCancel} | |||||
| onConfirm={handleConfirm} | |||||
| ></ParameterRange> | |||||
| } | |||||
| okText="确定" | |||||
| cancelText="取消" | |||||
| overlayClassName={styles['parameter-range']} | |||||
| icon={null} | |||||
| open={open} | |||||
| destroyTooltipOnHide | |||||
| > | |||||
| <div | |||||
| className={classNames(styles['parameter-range__input'], { | |||||
| [styles['parameter-range__input--disabled']]: disabled, | |||||
| })} | |||||
| onClick={handleClick} | |||||
| > | |||||
| <Typography.Text | |||||
| ellipsis={{ tooltip: jsonText }} | |||||
| style={{ color: 'inherit' }} | |||||
| className={styles['parameter-range__input__text']} | |||||
| > | |||||
| {jsonText} | |||||
| </Typography.Text> | |||||
| <KFIcon type="icon-bianji" className={styles['parameter-range__input__icon']} /> | |||||
| </div> | |||||
| </Popconfirm> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default PopParameterRange; | |||||
| @@ -0,0 +1,108 @@ | |||||
| .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: #c73131; | |||||
| font-size: 14px; | |||||
| font-family: SimSun, sans-serif; | |||||
| line-height: 1; | |||||
| content: '*'; | |||||
| margin-inline-end: 4px; | |||||
| } | |||||
| } | |||||
| &__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; | |||||
| border-radius: 8px; | |||||
| &__body { | |||||
| flex: 1; | |||||
| margin-right: 10px; | |||||
| padding: 20px 20px 0; | |||||
| border: 1px dashed @border-color-base; | |||||
| } | |||||
| &__operation { | |||||
| display: flex; | |||||
| flex: none; | |||||
| align-items: center; | |||||
| width: 100px; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,155 @@ | |||||
| export enum ParameterType { | |||||
| Uniform = 'uniform', | |||||
| QUniform = 'quniform', | |||||
| LogUniform = 'loguniform', | |||||
| QLogUniform = 'qloguniform', | |||||
| Randn = 'randn', | |||||
| QRandn = 'qrandn', | |||||
| RandInt = 'randint', | |||||
| QRandInt = 'qrandint', | |||||
| LogRandInt = 'lograndint', | |||||
| QLogRandInt = 'qlograndint', | |||||
| Choice = 'choice', | |||||
| Grid = 'grid', | |||||
| Range = 'range', | |||||
| Fixed = 'fixed', | |||||
| } | |||||
| export const parameterOptions = [ | |||||
| 'uniform', | |||||
| 'quniform', | |||||
| 'loguniform', | |||||
| 'qloguniform', | |||||
| 'randn', | |||||
| 'qrandn', | |||||
| 'randint', | |||||
| 'qrandint', | |||||
| 'lograndint', | |||||
| 'qlograndint', | |||||
| 'choice', | |||||
| 'grid', | |||||
| ].map((name) => ({ | |||||
| label: name, | |||||
| value: name, | |||||
| })); | |||||
| export const axParameterOptions = ['fixed', 'range', 'choice'].map((name) => ({ | |||||
| label: name, | |||||
| value: name, | |||||
| })); | |||||
| export type ParameterData = { | |||||
| label: string; | |||||
| name: string; | |||||
| value?: number; | |||||
| }; | |||||
| // 参数表单数据 | |||||
| export type FormParameter = { | |||||
| name: string; // 参数名称 | |||||
| type: ParameterType; // 参数类型 | |||||
| range: any; // 参数值 | |||||
| [key: string]: any; | |||||
| }; | |||||
| export const getFormOptions = (type?: ParameterType, value?: number[]): ParameterData[] => { | |||||
| const numbers = | |||||
| value?.map((item) => { | |||||
| const num = Number(item); | |||||
| if (isNaN(num)) { | |||||
| return undefined; | |||||
| } | |||||
| return num; | |||||
| }) ?? []; | |||||
| switch (type) { | |||||
| case ParameterType.Uniform: | |||||
| case ParameterType.LogUniform: | |||||
| case ParameterType.RandInt: | |||||
| case ParameterType.LogRandInt: | |||||
| case ParameterType.Range: | |||||
| return [ | |||||
| { | |||||
| name: 'min', | |||||
| label: '最小值', | |||||
| value: numbers?.[0], | |||||
| }, | |||||
| { | |||||
| name: 'max', | |||||
| label: '最大值', | |||||
| value: numbers?.[1], | |||||
| }, | |||||
| ]; | |||||
| case ParameterType.QUniform: | |||||
| case ParameterType.QLogUniform: | |||||
| case ParameterType.QRandInt: | |||||
| case ParameterType.QLogRandInt: | |||||
| return [ | |||||
| { | |||||
| name: 'min', | |||||
| label: '最小值', | |||||
| value: numbers?.[0], | |||||
| }, | |||||
| { | |||||
| name: 'max', | |||||
| label: '最大值', | |||||
| value: numbers?.[1], | |||||
| }, | |||||
| { | |||||
| name: 'q', | |||||
| label: '间隔', | |||||
| value: numbers?.[2], | |||||
| }, | |||||
| ]; | |||||
| case ParameterType.Randn: | |||||
| return [ | |||||
| { | |||||
| name: 'mean', | |||||
| label: '均值', | |||||
| value: numbers?.[0], | |||||
| }, | |||||
| { | |||||
| name: 'std', | |||||
| label: '方差', | |||||
| value: numbers?.[1], | |||||
| }, | |||||
| ]; | |||||
| case ParameterType.QRandn: | |||||
| return [ | |||||
| { | |||||
| name: 'mean', | |||||
| label: '均值', | |||||
| value: numbers?.[0], | |||||
| }, | |||||
| { | |||||
| name: 'std', | |||||
| label: '方差', | |||||
| value: numbers?.[1], | |||||
| }, | |||||
| { | |||||
| name: 'q', | |||||
| label: '间隔', | |||||
| value: numbers?.[2], | |||||
| }, | |||||
| ]; | |||||
| case ParameterType.Fixed: | |||||
| return [ | |||||
| { | |||||
| name: 'value', | |||||
| label: '值', | |||||
| value: numbers?.[0], | |||||
| }, | |||||
| ]; | |||||
| default: | |||||
| return []; | |||||
| } | |||||
| }; | |||||
| export const getReqParamName = (type: ParameterType) => { | |||||
| if (type === ParameterType.Fixed) { | |||||
| return 'value'; | |||||
| } else if (type === ParameterType.Choice || type === ParameterType.Grid) { | |||||
| return 'values'; | |||||
| } else { | |||||
| return 'bounds'; | |||||
| } | |||||
| }; | |||||
| @@ -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,64 @@ | |||||
| import { type ParameterInputObject } from '@/components/ResourceSelect'; | |||||
| import { type NodeStatus } from '@/types'; | |||||
| import { type FormParameter } from './components/CreateForm/utils'; | |||||
| // 操作类型 | |||||
| export enum OperationType { | |||||
| Create = 'Create', // 创建 | |||||
| Update = 'Update', // 更新 | |||||
| } | |||||
| // 表单数据 | |||||
| export type FormData = { | |||||
| name: string; // 实验名称 | |||||
| description: string; // 实验描述 | |||||
| code: ParameterInputObject; // 代码 | |||||
| dataset: ParameterInputObject; // 数据集 | |||||
| dataset_path: string; // 数据集路径 | |||||
| main_py: string; // 主函数代码文件 | |||||
| metrics: string; // 指标 | |||||
| mode: string; // 优化方向 | |||||
| search_alg?: string; // 搜索算法 | |||||
| scheduler?: string; // 调度算法 | |||||
| num_samples: number; // 总实验次数 | |||||
| max_t: number; // 单次试验最大时间 | |||||
| min_samples_required: number; // 计算中位数的最小试验数 | |||||
| cpu: number; // cpu 数 | |||||
| gpu: number; // gpu 数 | |||||
| parameters: FormParameter[]; | |||||
| points_to_evaluate: { [key: string]: any }[]; | |||||
| }; | |||||
| export type HyperparameterData = { | |||||
| id: number; | |||||
| progress: number; | |||||
| run_state: string; | |||||
| state: number; | |||||
| create_by?: string; | |||||
| create_time?: string; | |||||
| update_by?: string; | |||||
| update_time?: string; | |||||
| status_list: string; // 最近五次运行状态 | |||||
| } & FormData; | |||||
| // 自动机器学习实验实例 | |||||
| export type AutoMLInstanceData = { | |||||
| 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; | |||||
| }; | |||||
| @@ -0,0 +1,93 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-11-18 10:18:27 | |||||
| * @Description: 超参数自动寻优请求 | |||||
| */ | |||||
| import { request } from '@umijs/max'; | |||||
| // 分页查询超参数自动寻优 | |||||
| export function getRayListReq(params) { | |||||
| return request(`/api/mmp/ray`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 查询超参数自动寻优详情 | |||||
| export function getRayInfoReq(params) { | |||||
| return request(`/api/mmp/ray/getRayDetail`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 新增超参数自动寻优 | |||||
| export function addRayReq(data) { | |||||
| return request(`/api/mmp/ray`, { | |||||
| method: 'POST', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 编辑超参数自动寻优 | |||||
| export function updateRayReq(data) { | |||||
| return request(`/api/mmp/ray`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 删除超参数自动寻优 | |||||
| export function deleteRayReq(id) { | |||||
| return request(`/api/mmp/ray/${id}`, { | |||||
| method: 'DELETE', | |||||
| }); | |||||
| } | |||||
| // 运行超参数自动寻优 | |||||
| export function runRayReq(id) { | |||||
| return request(`/api/mmp/ray/run/${id}`, { | |||||
| method: 'POST', | |||||
| }); | |||||
| } | |||||
| // ----------------------- 实验实例 ----------------------- | |||||
| // 获取实验实例列表 | |||||
| export function getRayInsListReq(params) { | |||||
| return request(`/api/mmp/rayIns`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 查询实验实例详情 | |||||
| export function getRayInsReq(id) { | |||||
| return request(`/api/mmp/rayIns/${id}`, { | |||||
| method: 'GET', | |||||
| }); | |||||
| } | |||||
| // 停止实验实例 | |||||
| export function stopRayInsReq(id) { | |||||
| return request(`/api/mmp/rayIns/${id}`, { | |||||
| method: 'PUT', | |||||
| }); | |||||
| } | |||||
| // 删除实验实例 | |||||
| export function deleteRayInsReq(id) { | |||||
| return request(`/api/mmp/rayIns/${id}`, { | |||||
| method: 'DELETE', | |||||
| }); | |||||
| } | |||||
| // 批量删除实验实例 | |||||
| export function batchDeleteRayInsReq(data) { | |||||
| return request(`/api/mmp/rayIns/batchDelete`, { | |||||
| method: 'DELETE', | |||||
| data | |||||
| }); | |||||
| } | |||||
| @@ -0,0 +1,3 @@ | |||||
| export const xlCols = { span: 12 }; | |||||
| export const xllCols = { span: 10 }; | |||||
| export const formCols = { xl: xlCols, xxl: xllCols }; | |||||