+
+
+}
diff --git a/ymir/web/src/components/form/editBox.js b/ymir/web/src/components/form/editBox.js
index eee2eb6f0a..d3b013fc17 100644
--- a/ymir/web/src/components/form/editBox.js
+++ b/ymir/web/src/components/form/editBox.js
@@ -32,6 +32,7 @@ const EditBox = ({ children, record, max=50, action = () => { } }) => {
onCancel={onCancel}
onOk={onOk}
destroyOnClose
+ forceRender
>
+
+export default EvaluationSelector
diff --git a/ymir/web/src/components/form/gtSelector.js b/ymir/web/src/components/form/gtSelector.js
new file mode 100644
index 0000000000..39b090ec25
--- /dev/null
+++ b/ymir/web/src/components/form/gtSelector.js
@@ -0,0 +1,15 @@
+import t from "@/utils/t"
+import CheckboxSelector from "./checkboxSelector"
+
+const types = [
+ { label: 'annotation.gt', value: 'gt', checked: true, },
+ { label: 'annotation.pred', value: 'pred', },
+]
+
+const GtSelector = props => ({ ...type, label: t(type.label)}))}
+ label={t('dataset.assets.selector.gt.label')}
+ {...props}
+/>
+
+export default GtSelector
diff --git a/ymir/web/src/components/form/imageSelect.js b/ymir/web/src/components/form/imageSelect.js
index cfeb3d085c..d7204f20ca 100644
--- a/ymir/web/src/components/form/imageSelect.js
+++ b/ymir/web/src/components/form/imageSelect.js
@@ -1,11 +1,14 @@
-import { Select } from 'antd'
+import { Col, Row, Select } from 'antd'
import { connect } from 'dva'
import { useEffect, useState } from 'react'
import { TYPES } from '@/constants/image'
+import { HIDDENMODULES } from '@/constants/common'
import t from '@/utils/t'
-const ImageSelect = ({ value, relatedId, type = TYPES.TRAINING, onChange = () => {}, getImages, getImage, ...resProps }) => {
+const getValue = image => image.id + ',' + image.url
+
+const ImageSelect = ({ value, relatedId, type = TYPES.TRAINING, onChange = () => { }, getImages, getImage, ...resProps }) => {
const [options, setOptions] = useState([])
useEffect(() => {
@@ -14,7 +17,12 @@ const ImageSelect = ({ value, relatedId, type = TYPES.TRAINING, onChange = () =>
useEffect(() => {
if (options.length === 1) {
- value = options[0].value
+ if (value) {
+ const opt = options.find(({ image }) => getValue(image) === value)
+ opt && onChange(value, opt.image)
+ } else {
+ value = options[0].value
+ }
}
}, [options])
@@ -32,9 +40,12 @@ const ImageSelect = ({ value, relatedId, type = TYPES.TRAINING, onChange = () =>
}
const generateOption = image => ({
- label: image.name,
+ label:
+ {image.name}
+ {!HIDDENMODULES.LIVECODE ? {t(`image.livecode.label.${image.liveCode ? 'remote' : 'local'}`)} : null}
+
,
image,
- value: image.id + ',' + image.url,
+ value: getValue(image),
})
async function generateOptions(images) {
@@ -59,14 +70,16 @@ const ImageSelect = ({ value, relatedId, type = TYPES.TRAINING, onChange = () =>
async function getRelatedOptions() {
const trainImage = await getImage(relatedId)
let relatedOptions = []
- if(trainImage?.related) {
+ if (trainImage?.related) {
relatedOptions = trainImage.related.map(generateOption)
}
return relatedOptions
}
return (
- onChange(value, opt?.image)} options={options} optionFilterProp="label" allowClear>
+ onChange(value, opt?.image)} options={options}
+ >
)
}
diff --git a/ymir/web/src/components/form/inferResultSelect.js b/ymir/web/src/components/form/inferResultSelect.js
new file mode 100644
index 0000000000..4a289991ce
--- /dev/null
+++ b/ymir/web/src/components/form/inferResultSelect.js
@@ -0,0 +1,200 @@
+import { Button, Checkbox, Col, Form, Row, Select, Tooltip } from 'antd'
+import { connect } from 'dva'
+import { useCallback, useEffect, useState } from 'react'
+
+import t from '@/utils/t'
+import useFetch from '@/hooks/useFetch'
+import ModelSelect from './modelSelect'
+import DatasetSelect from './datasetSelect'
+import { useHistory } from 'umi'
+
+const sameConfig = (config, config2) => {
+ return JSON.stringify(config2) === JSON.stringify(config)
+}
+const sameConfigs = (config, configs) => {
+ return configs.some(item => sameConfig(item, config))
+}
+
+const ConfigSelect = ({ value, configs = [], onChange = () => { } }) => {
+ const [options, setOptions] = useState([])
+
+ useEffect(() => {
+ const opts = configs.map((item, index) => {
+ const title = [...item.model, JSON.stringify(item.config)].join('\n')
+ return {
+ value: index,
+ label: {item.name} ,
+ config: item,
+ }
+ })
+ setOptions(opts)
+ }, [configs])
+
+ useEffect(() => {
+ if (value) {
+ change(value, options.filter(opt => value.includes(opt.value)))
+ }
+ }, [options])
+
+ const change = (values) => {
+ onChange(values, values.map(index => options[index]))
+ }
+
+ return
+}
+
+const InferResultSelect = ({ pid, form, value, onChange = () => { } }) => {
+ const history = useHistory()
+ const [models, setModels] = useState([])
+ const [datasets, setDatasets] = useState([])
+ const [testingDatasets, setTestingDatasets] = useState([])
+ const [selectedDatasets, setSelectedDatasets] = useState([])
+ const [configs, setConfigs] = useState([])
+ const [selectedConfigs, setSelectedConfigs] = useState([])
+ const [inferTasks, fetchInferTask] = useFetch('task/queryInferTasks', [])
+ const [fetched, setFetched] = useState(false)
+ const [selectedTasks, setSelectedTasks] = useState([])
+ const [tasks, setTasks] = useState([])
+ const selectedStages = Form.useWatch('stage', form)
+
+ useEffect(() => {
+ setTasks(inferTasks)
+ setFetched(true)
+ }, [inferTasks])
+
+ useEffect(() => {
+ const stages = selectedStages?.map(([model, stage]) => stage) || []
+ if (stages.length) {
+ fetchInferTask({ stages })
+ } else {
+ setTasks([])
+ setFetched(false)
+ }
+ setSelectedDatasets([])
+ form.setFieldsValue({ dataset: undefined, config: undefined })
+ }, [selectedStages])
+
+ useEffect(() => {
+ if (datasets.length === 1) {
+ form.setFieldsValue({ dataset: datasets })
+ }
+ setConfigs([])
+ form.setFieldsValue({ config: undefined })
+ }, [datasets])
+
+ useEffect(() => {
+ form.setFieldsValue({ config: configs.length === 1 ? [0] : undefined })
+ }, [configs])
+
+ useEffect(() => {
+ const testingDatasets = tasks.map(({ parameters: { dataset_id } }) => dataset_id)
+ const crossDatasets = testingDatasets.filter(dataset => {
+ const targetTasks = tasks.filter(({ parameters: { dataset_id } }) => dataset_id === dataset)
+ return selectedStages.every(([model, stage]) => targetTasks.map(({ parameters: { model_stage_id } }) => model_stage_id).includes(stage))
+ })
+ setDatasets([...new Set(crossDatasets)])
+ }, [tasks])
+
+ useEffect(() => {
+ const configs = tasks
+ .filter(({ parameters: { dataset_id } }) => (selectedDatasets ? selectedDatasets.includes(dataset_id) : true))
+ .reduce((prev, { config, parameters: { model_id, model_stage_id } }) => {
+ const stageName = getStageName([model_id, model_stage_id])
+ return sameConfigs(config, prev.map(({ config }) => config)) ?
+ prev.map(item => {
+ sameConfig(item.config, config) && item.model.push(stageName)
+ return item
+ }) :
+ [...prev, { config, model: [stageName] }]
+ }, [])
+ setConfigs(configs.map((config, index) => ({ ...config, name: `config${index + 1}` })))
+ }, [tasks, selectedDatasets])
+
+ useEffect(() => {
+ form.setFieldsValue({ config: configs.map((_, index) => index) })
+ }, [configs])
+
+ useEffect(() => {
+ const selected = []
+ selectedStages?.forEach(([model, selectedStage]) => {
+ selectedDatasets.forEach(did => {
+ const dtask = tasks.filter(({
+ parameters: { dataset_id, model_stage_id: stage }
+ }) => dataset_id === did && stage === selectedStage)
+ selectedConfigs.forEach(({ config: sconfig, name }) => {
+ const ctask = dtask.find(({ config }) => sameConfig(config, sconfig))
+ ctask && selected.push({ ...ctask, configName: name })
+ })
+ })
+ })
+ setSelectedTasks(selected)
+ }, [tasks, selectedConfigs])
+
+ useEffect(() => {
+ onChange({
+ tasks: selectedTasks,
+ models,
+ datasets: testingDatasets,
+ })
+ }, [selectedTasks])
+
+ function getStageName([model, stage]) {
+ const m = models.find(md => md.id === model)
+ let s = {}
+ if (m) {
+ s = m.stages.find(sg => sg.id === stage)
+ }
+ return m && s ? `${m.name} ${m.versionName} ${s.name}` : ''
+ }
+
+ function modelChange(values, options = []) {
+ // setSelectedStages(values)
+ setModels(options.map(([opt]) => opt?.model))
+ }
+
+ function datasetChange(values, options = []) {
+ setSelectedDatasets(values)
+ setTestingDatasets(options.map(({ dataset }) => dataset))
+ }
+
+ function configChange(values, options = []) {
+ setSelectedConfigs(options.map((opt) => opt ? opt.config : null))
+ }
+
+ const filterDatasets = useCallback((all) => {
+ return all.filter(({ id }) => datasets.includes(id))
+ }, [datasets])
+
+ const goInfer = useCallback(() => {
+ const mids = selectedStages?.map(String)?.join('|')
+ const query = selectedStages?.length ? `?mid=${mids}` : ''
+ history.push(`/home/project/${pid}/inference${query}`)
+ }, [selectedStages])
+
+ const renderInferBtn =
+ {t('task.infer.diagnose.tip')}
+ {t('common.action.inference')}
+
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+const props = (state) => {
+ return {
+ allModels: state.model.allModels,
+ }
+}
+
+export default connect(props, null)(InferResultSelect)
diff --git a/ymir/web/src/components/form/items/datasetName.js b/ymir/web/src/components/form/items/datasetName.js
new file mode 100644
index 0000000000..b9d399bdc6
--- /dev/null
+++ b/ymir/web/src/components/form/items/datasetName.js
@@ -0,0 +1,24 @@
+import { Form, Input } from "antd"
+import t from '@/utils/t'
+
+const DatasetName = ({
+ name = 'name',
+ itemProps = {},
+ inputProps = {},
+ prefix = 'dataset.add.form.name',
+}) => {
+ const rules = [
+ { required: true, whitespace: true, message: t(`${prefix}.required`) },
+ { type: 'string', min: 2, max: 80 },
+ ]
+ return
+
+
+}
+
+export default DatasetName
diff --git a/ymir/web/src/components/form/items/dockerConfig.js b/ymir/web/src/components/form/items/dockerConfig.js
new file mode 100644
index 0000000000..9c740f76d9
--- /dev/null
+++ b/ymir/web/src/components/form/items/dockerConfig.js
@@ -0,0 +1,103 @@
+import { useEffect, useState } from "react"
+import { Col, Form, Input, InputNumber, Row, Space } from "antd"
+import Panel from "@/components/form/panel"
+import t from '@/utils/t'
+import s from "./form.less"
+import PreProcessForm from "./preProcess"
+import { AddTwoIcon, AddDelTwoIcon } from '@/components/common/icons'
+function getArrayConfig(config = {}) {
+ const excludes = ['gpu_count', 'task_id']
+ return Object.keys(config)
+ .filter(key => !excludes.includes(key))
+ .map(key => ({
+ key,
+ value: config[key]
+ }))
+}
+
+const DockerConfigForm = ({ show, form, seniorConfig, name = 'hyperparam' }) => {
+ const [visible, setVisible] = useState(false)
+ const [config, setConfig] = useState([])
+ const hyperParams = Form.useWatch('hyperparam', form)
+
+ useEffect(() => setConfig(getArrayConfig(seniorConfig)), [seniorConfig])
+
+ useEffect(() => form.setFieldsValue({ [name]: config }), [config])
+
+ useEffect(() => setVisible(show), [show])
+
+ async function validHyperParams(rule, value) {
+
+ const params = hyperParams.map(({ key }) => key)
+ .filter(item => item && item.trim() && item === value)
+ if (params.length > 1) {
+ return Promise.reject(t('task.validator.same.param'))
+ } else {
+ return Promise.resolve()
+ }
+ }
+ const renderTitle = <>
+ {t('task.train.form.hyperparam.label')}
+ {t('task.train.form.hyperparam.label.tip')}
+ >
+
+ return config.length ?
+
+
+
+ {(fields, { add, remove }) => (
+ <>
+
+ >
+ )}
+
+
+
+
+ : null
+}
+
+export default DockerConfigForm
diff --git a/ymir/web/src/pages/task/train/index.less b/ymir/web/src/components/form/items/form.less
similarity index 100%
rename from ymir/web/src/pages/task/train/index.less
rename to ymir/web/src/components/form/items/form.less
diff --git a/ymir/web/src/components/form/items/liveCode.js b/ymir/web/src/components/form/items/liveCode.js
new file mode 100644
index 0000000000..ce8840e7ce
--- /dev/null
+++ b/ymir/web/src/components/form/items/liveCode.js
@@ -0,0 +1,56 @@
+import Panel from "@/components/form/panel"
+import { Button, Col, Form, Input, Row } from "antd"
+import t from '@/utils/t'
+import { getConfigUrl } from "./liveCodeConfig"
+import { useEffect, useState } from "react"
+
+
+const LiveCodeForm = ({ form, live }) => {
+ const url = Form.useWatch(['live', 'git_url'], form)
+ const id = Form.useWatch(['live', 'git_branch'], form)
+ const config = Form.useWatch(['live', 'code_config'], form)
+ const [configUrl, setConfigUrl] = useState('')
+
+ useEffect(() => {
+ if (url && id && config) {
+ const configUrl = getConfigUrl({
+ git_url: url,
+ git_branch: id,
+ code_config: config,
+ })
+ setConfigUrl(configUrl)
+ } else {
+ setConfigUrl('')
+ }
+ }, [url, id, config])
+
+ return live ?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('common.view')}
+
+
+ : null
+}
+
+export default LiveCodeForm
diff --git a/ymir/web/src/components/form/items/liveCodeConfig.js b/ymir/web/src/components/form/items/liveCodeConfig.js
new file mode 100644
index 0000000000..30f72f9d49
--- /dev/null
+++ b/ymir/web/src/components/form/items/liveCodeConfig.js
@@ -0,0 +1,26 @@
+
+export const FIELDS = Object.freeze([
+ { key: 'url', field: 'git_url', },
+ { key: 'id', field: 'git_branch', },
+ { key: 'config', field: 'code_config', },
+])
+
+export const getConfigUrl = (config = {}) => {
+ const getField = index => config[FIELDS[index].field] || ''
+ const base = getField(0).replace(/(\.git)?$/, '')
+ const commit = getField(1)
+ const configFile = getField(2)
+ const url = `${base}/blob/${commit}/${configFile}`
+ return url
+}
+
+export const isLiveCode = config => FIELDS.reduce((prev, curr) => prev && config[curr.field], true)
+
+export function removeLiveCodeConfig(config = {}) {
+ return Object.keys(config).reduce((prev, key) => FIELDS.map(({ field }) => field).includes(key) ?
+ prev :
+ {
+ ...prev,
+ [key]: config[key],
+ }, {})
+}
\ No newline at end of file
diff --git a/ymir/web/src/components/form/items/openpai.js b/ymir/web/src/components/form/items/openpai.js
new file mode 100644
index 0000000000..de6412c7f0
--- /dev/null
+++ b/ymir/web/src/components/form/items/openpai.js
@@ -0,0 +1,21 @@
+import { Form, Radio } from "antd"
+import t from '@/utils/t'
+import { useEffect } from "react"
+
+const types = [
+ { value: true, label: 'common.yes', },
+ { value: false, label: 'common.no', checked: true, },
+]
+
+const OpenpaiForm = ({ form, openpai }) => {
+
+ useEffect(() => {
+ form.setFieldsValue({ openpai: openpai })
+ }, [openpai])
+
+ return openpai ?
+ ({ ...type, label: t(type.label) }))} />
+ : null
+}
+
+export default OpenpaiForm
diff --git a/ymir/web/src/components/form/items/preProcess.js b/ymir/web/src/components/form/items/preProcess.js
new file mode 100644
index 0000000000..a6425d26dc
--- /dev/null
+++ b/ymir/web/src/components/form/items/preProcess.js
@@ -0,0 +1,37 @@
+import { Checkbox, Form, InputNumber } from "antd"
+import t from '@/utils/t'
+import { useState } from "react"
+
+const funcs = [
+ {
+ func: 'longside_resize',
+ label: 'task.train.preprocess.resize',
+ params: [{
+ key: 'dest_size',
+ rules: [{ required: true, message: t('task.train.preprocess.resize.placeholder'), }],
+ component:
+ }],
+ }
+]
+
+const PreProcessForm = () => {
+ const [selected, setSelected] = useState([])
+ const renderTitle = (func, label) => <>
+
+ {t(label)}
+ >
+ function preprocessSelected({ target: { value, checked } }) {
+ setSelected(old => ({ ...old, [value]: checked }))
+ }
+ return funcs.map(({ func, label, params }) =>
+
+ {selected[func] ? params.map(({ key, rules, component }) => (
+
+ {component}
+
+ )) : null}
+
+ )
+}
+
+export default PreProcessForm
diff --git a/ymir/web/src/components/form/keywordSelect.js b/ymir/web/src/components/form/keywordSelect.js
new file mode 100644
index 0000000000..2e776b0635
--- /dev/null
+++ b/ymir/web/src/components/form/keywordSelect.js
@@ -0,0 +1,66 @@
+import { Col, Row, Select } from 'antd'
+import { useEffect, useState } from 'react'
+
+import t from '@/utils/t'
+import useFetch from '@/hooks/useFetch'
+
+
+const KeywordSelect = ({ value, onChange = () => { }, keywords, filter, ...resProps }) => {
+ const [options, setOptions] = useState([])
+ const [keywordResult, getKeywords] = useFetch('keyword/getKeywords')
+
+ useEffect(() => {
+ if (keywords) {
+ generateOptions(keywords.map(keyword => ({ name: keyword })))
+ } else {
+ getKeywords({ limit: 9999 })
+ }
+ }, [keywords])
+
+ useEffect(() => {
+ if (options.length) {
+ if (value) {
+ onChange(value, resProps.mode ? options.filter(opt => value.includes(opt.value)) : options.find(opt => opt.value === value))
+ }
+ }
+ }, [options])
+
+ useEffect(() => {
+ if (options.length === 1) {
+ value = options[0].value
+ }
+ }, [options])
+
+ useEffect(() => {
+ if (keywordResult) {
+ generateOptions(keywordResult.items)
+ }
+ }, [keywordResult])
+
+ function generateOptions(keywords = []) {
+ filter = filter || (x => x)
+ const opts = filter(keywords).map(keyword => ({
+ label: {keyword.name}
,
+ aliases: keyword.aliases,
+ value: keyword.name,
+ }))
+ setOptions(opts)
+ }
+
+ function filterOptions(options, filter = x => x) {
+ return filter(options)
+ }
+
+ return (
+ [option.value, ...(option.aliases || [])].some(key => key.indexOf(value) >= 0)}
+ options={filterOptions(options, filter)}
+ onChange={onChange}
+ {...resProps}
+ >
+ )
+}
+
+export default KeywordSelect
diff --git a/ymir/web/src/components/form/modelSelect.js b/ymir/web/src/components/form/modelSelect.js
index 509368ffa7..1da0f84793 100644
--- a/ymir/web/src/components/form/modelSelect.js
+++ b/ymir/web/src/components/form/modelSelect.js
@@ -1,76 +1,80 @@
-import { Col, Row, Select } from 'antd'
-import { connect } from 'dva'
+import { Cascader, ConfigProvider } from 'antd'
+import { useSelector } from 'umi'
import { useEffect, useState } from 'react'
import { percent } from '@/utils/number'
+import t from '@/utils/t'
+import useFetch from '@/hooks/useFetch'
+import EmptyStateModel from '@/components/empty/model'
-
-const ModelSelect = ({ pid, value, allModels, onChange = () => { }, getModels, ...resProps }) => {
+const ModelSelect = ({
+ pid, value, onlyModel, changeByUser,
+ onChange = () => { }, onReady = () => { },
+ filters, ...resProps
+}) => {
+ const models = useSelector(state => state.model.allModels)
+ const [ms, setMS] = useState(null)
const [options, setOptions] = useState([])
- const [models, setModels] = useState([])
+ const [_, getModels] = useFetch('model/queryAllModels')
- useEffect(() => {
- fetchModels()
- }, [])
+ useEffect(() => pid && getModels(pid), [pid])
- useEffect(() => {
- if (options.length) {
- if (value) {
- onChange(value, resProps.mode ? options.filter(opt => value.includes(opt.value)) : options.find(opt => opt.value === value))
- }
- }
- }, [options])
+ useEffect(() => setMS(value), [value])
- useEffect(() => {
- setModels(allModels)
- }, [allModels])
+ useEffect(() => onReady(models), [models])
useEffect(() => {
- if (options.length === 1) {
- value = options[0].value
+ if (options.length && value && !changeByUser) {
+ let selected = null
+ if (resProps.multiple) {
+ selected = options.filter(opt => value.some(([model]) => opt.model.id === model)).map(opt => [opt, opt.value])
+ } else {
+ const opt = options.find(opt => opt?.model?.id === value[0])
+ selected = opt ? [opt, value[1] || opt?.model?.recommendStage] : undefined
+ }
+ if (!selected) {
+ onChange([], undefined)
+ setMS([])
+ } else {
+ onChange(value, selected)
+ }
}
}, [options])
- useEffect(() => {
- generateOptions()
- }, [models])
-
- function fetchModels() {
- getModels(pid)
- }
+ useEffect(() => generateOptions(), [models])
function generateOptions() {
- const opts = models.map(model => {
+ const mds = filters ? filters(models) : models
+ const opts = mds.map(model => {
+ const name = `${model.name} ${model.versionName}`
+ const childrenNode = onlyModel ? {} : {
+ children: model.stages.map(stage => ({
+ label: ` ${stage.name} (mAP:${percent(stage.map)}) ${stage.id === model.recommendStage ? t('common.recommend') : ''}`,
+ value: stage.id,
+ }))
+ }
return {
- label:
- {model.name} {model.versionName}
- mAP: {percent(model.map)}
-
,
+ label: name,
model,
value: model.id,
+ ...childrenNode,
}
})
setOptions(opts)
}
+ function filter(input, path) {
+ return path.some(({ label = '' }) => label.toLowerCase().indexOf(input.toLowerCase()) > -1)
+ }
+
return (
-
+ }>
+ label.join('/')}
+ showCheckedStrategy={Cascader.SHOW_CHILD} showSearch={{ filter }}
+ placeholder={t('task.train.form.model.placeholder')}
+ allowClear {...resProps}>
+
)
}
-const props = (state) => {
- return {
- allModels: state.model.allModels,
- }
-}
-const actions = (dispatch) => {
- return {
- getModels(pid) {
- return dispatch({
- type: 'model/queryAllModels',
- payload: pid,
- })
- }
- }
-}
-export default connect(props, actions)(ModelSelect)
+export default ModelSelect
diff --git a/ymir/web/src/components/form/panel.js b/ymir/web/src/components/form/panel.js
index dc09baf107..00ff648369 100644
--- a/ymir/web/src/components/form/panel.js
+++ b/ymir/web/src/components/form/panel.js
@@ -1,18 +1,26 @@
import { Col, Row } from "antd"
import { ArrowDownIcon, ArrowRightIcon } from '@/components/common/icons'
import s from './panel.less'
+import { useEffect, useState } from "react"
-const Panel = ({ hasHeader = true, toogleVisible = true, visible = false, setVisible = () => { }, label = '', children }) => {
+const Panel = ({ hasHeader = true, toogleVisible = true, visible, setVisible = () => { }, label = '', bg = true, children }) => {
+ const [vis, setVis] = useState(false)
+ useEffect(() => {
+ setVis(visible)
+ }, [visible])
return (
- {hasHeader ?
setVisible(!visible)}>
+ {hasHeader ? {
+ setVis(!vis)
+ setVisible(!vis)
+ }}>
{label}
{toogleVisible ?
- {visible ? : }
+ {vis ? : }
: null}
: null}
-
diff --git a/ymir/web/src/components/form/radioGroup.js b/ymir/web/src/components/form/radioGroup.js
new file mode 100644
index 0000000000..00e047135b
--- /dev/null
+++ b/ymir/web/src/components/form/radioGroup.js
@@ -0,0 +1,12 @@
+import { Radio } from "antd"
+import t from '@/utils/t'
+
+const RadioGroup = ({ value, onChange = () => { }, options, labelPrefix = '' }) => (
+ ({ ...item, label: t(`${labelPrefix}${item.label}`) }))}
+ value={value}
+ onChange={onChange}
+ >
+)
+
+export default RadioGroup
diff --git a/ymir/web/src/components/form/tip.js b/ymir/web/src/components/form/tip.js
index 9e8f715f93..caea63ce98 100644
--- a/ymir/web/src/components/form/tip.js
+++ b/ymir/web/src/components/form/tip.js
@@ -1,14 +1,18 @@
-import { Col, Row } from "antd"
+import { FailIcon, TipsIcon } from "@/components/common/icons"
+import s from './tip.less'
-import SingleTip from './singleTip'
+const Tip = ({ type = 'success', content = '' }) => {
+ return content ? (
+
+ {getIcon(type)}
+ {content}
+
+ ) : null
+}
-const Tip = ({ title = null, content = '', placement = 'right', span=6, formSpan=0, hidden = false, children }) => {
- return (
-
- {children}
- {hidden ? null : }
-
- )
+function getIcon(type) {
+ const cls = `${s.icon} ${s[type]}`
+ return type === 'success' ? :
}
export default Tip
diff --git a/ymir/web/src/components/form/tip.less b/ymir/web/src/components/form/tip.less
index cc80197f9d..702f300018 100644
--- a/ymir/web/src/components/form/tip.less
+++ b/ymir/web/src/components/form/tip.less
@@ -1,4 +1,28 @@
-.icon {
- cursor: pointer;
- color: rgba(0, 0, 0, 0.25);
+.tipContainer {
+ @error: rgb(242, 99, 123);
+ @success: @primary-color;
+ color: rgba(0, 0, 0, 0.65);
+
+ &.error, &.success {
+ border-radius: 2px;
+ line-height: 40px;
+ padding: 0 10px;
+ }
+ &.success {
+ background: fade(@success, 10);
+ border: 1px solid @success;
+ }
+ &.error {
+ background: fade(@error, 10);
+ border: 1px solid @error;
+ }
+ .success, .error {
+ margin-right: 10px;
+ }
+ .success {
+ color: @success;
+ }
+ .error {
+ color: @error;
+ }
}
\ No newline at end of file
diff --git a/ymir/web/src/components/form/uploader.js b/ymir/web/src/components/form/uploader.js
index f981527b5f..1e6988e04f 100644
--- a/ymir/web/src/components/form/uploader.js
+++ b/ymir/web/src/components/form/uploader.js
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'
import ImgCrop from 'antd-img-crop'
import { CloudUploadOutlined } from "@ant-design/icons"
-import { getUploadUrl } from "../../services/common"
+import { getUploadUrl } from "@/services/common"
import storage from '@/utils/storage'
import t from '@/utils/t'
import 'antd/es/slider/style'
@@ -16,8 +16,12 @@ const fileSuffix = {
all: ['*'],
}
-function Uploader({ className, value=null, format="zip", label, max = 200,
- maxCount = 1, info = '', type='', crop = false, showUploadList = true, onChange = ()=> {}, onRemove = () => {}}) {
+function Uploader({
+ className, value = null, format = "zip", label, max = 200,
+ maxCount = 1, info = '', type = 'primary', crop = false,
+ btnProps = {},
+ showUploadList = true, onChange = () => { }, onRemove = () => { }
+}) {
label = label || t('model.add.form.upload.btn')
const [files, setFiles] = useState(null)
@@ -61,25 +65,25 @@ function Uploader({ className, value=null, format="zip", label, max = 200,
}
const uploader =
- }>{label}
-
+ className={className}
+ fileList={files}
+ action={getUploadUrl()}
+ name='file'
+ headers={{ "Authorization": `Bearer ${storage.get("access_token")}` }}
+ // accept={fileSuffix[format].join(',')}
+ onChange={onFileChange}
+ onRemove={onRemove}
+ beforeUpload={beforeUpload}
+ maxCount={maxCount}
+ showUploadList={showUploadList}
+ >
+ } {...btnProps}>{label}
+
return (
<>
- { format === 'avatar' && crop ? {uploader} : uploader}
- {info ? {info}
: null }
+ {format === 'avatar' && crop ? {uploader} : uploader}
+ {info ? {info}
: null}
>
)
}
diff --git a/ymir/web/src/components/icon/Bushu.tsx b/ymir/web/src/components/icon/Bushu.tsx
new file mode 100644
index 0000000000..a3f9f940ce
--- /dev/null
+++ b/ymir/web/src/components/icon/Bushu.tsx
@@ -0,0 +1,49 @@
+import React, { useEffect, useRef } from 'react';
+import styles from './style.css';
+interface IconProps extends React.SVGProps {
+ size?: string | number;
+ width?: string | number;
+ height?: string | number;
+ spin?: boolean;
+ rtl?: boolean;
+ color?: string;
+ fill?: string;
+ stroke?: string;
+}
+
+export default function Bushu(props: IconProps) {
+ const root = useRef(null)
+ const { size = '1em', width, height, spin, rtl, color, fill, stroke, className, ...rest } = props;
+ const _width = width || size;
+ const _height = height || size;
+ const _stroke = stroke || color;
+ const _fill = fill || color;
+ useEffect(() => {
+ if (!_fill) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-fill]').forEach(item => {
+ item.setAttribute('fill', item.getAttribute('data-follow-fill') || '')
+ })
+ }
+ if (!_stroke) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-stroke]').forEach(item => {
+ item.setAttribute('stroke', item.getAttribute('data-follow-stroke') || '')
+ })
+ }
+ }, [stroke, color, fill])
+ return (
+
+
+
+ )
+}
diff --git a/ymir/web/src/components/icon/New.tsx b/ymir/web/src/components/icon/New.tsx
new file mode 100644
index 0000000000..5d886d51a6
--- /dev/null
+++ b/ymir/web/src/components/icon/New.tsx
@@ -0,0 +1,49 @@
+import React, { useEffect, useRef } from 'react';
+import styles from './style.css';
+interface IconProps extends React.SVGProps {
+ size?: string | number;
+ width?: string | number;
+ height?: string | number;
+ spin?: boolean;
+ rtl?: boolean;
+ color?: string;
+ fill?: string;
+ stroke?: string;
+}
+
+export default function New(props: IconProps) {
+ const root = useRef(null)
+ const { size = '1em', width, height, spin, rtl, color, fill, stroke, className, ...rest } = props;
+ const _width = width || size;
+ const _height = height || size;
+ const _stroke = stroke || color;
+ const _fill = fill || color;
+ useEffect(() => {
+ if (!_fill) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-fill]').forEach(item => {
+ item.setAttribute('fill', item.getAttribute('data-follow-fill') || '')
+ })
+ }
+ if (!_stroke) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-stroke]').forEach(item => {
+ item.setAttribute('stroke', item.getAttribute('data-follow-stroke') || '')
+ })
+ }
+ }, [stroke, color, fill])
+ return (
+
+
+
+ )
+}
diff --git a/ymir/web/src/components/icon/Redu.tsx b/ymir/web/src/components/icon/Redu.tsx
new file mode 100644
index 0000000000..e85190ff09
--- /dev/null
+++ b/ymir/web/src/components/icon/Redu.tsx
@@ -0,0 +1,49 @@
+import React, { useEffect, useRef } from 'react';
+import styles from './style.css';
+interface IconProps extends React.SVGProps {
+ size?: string | number;
+ width?: string | number;
+ height?: string | number;
+ spin?: boolean;
+ rtl?: boolean;
+ color?: string;
+ fill?: string;
+ stroke?: string;
+}
+
+export default function Redu(props: IconProps) {
+ const root = useRef(null)
+ const { size = '1em', width, height, spin, rtl, color, fill, stroke, className, ...rest } = props;
+ const _width = width || size;
+ const _height = height || size;
+ const _stroke = stroke || color;
+ const _fill = fill || color;
+ useEffect(() => {
+ if (!_fill) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-fill]').forEach(item => {
+ item.setAttribute('fill', item.getAttribute('data-follow-fill') || '')
+ })
+ }
+ if (!_stroke) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-stroke]').forEach(item => {
+ item.setAttribute('stroke', item.getAttribute('data-follow-stroke') || '')
+ })
+ }
+ }, [stroke, color, fill])
+ return (
+
+
+
+ )
+}
diff --git a/ymir/web/src/components/icon/Shangjia.tsx b/ymir/web/src/components/icon/Shangjia.tsx
new file mode 100644
index 0000000000..aba0cd961c
--- /dev/null
+++ b/ymir/web/src/components/icon/Shangjia.tsx
@@ -0,0 +1,49 @@
+import React, { useEffect, useRef } from 'react';
+import styles from './style.css';
+interface IconProps extends React.SVGProps {
+ size?: string | number;
+ width?: string | number;
+ height?: string | number;
+ spin?: boolean;
+ rtl?: boolean;
+ color?: string;
+ fill?: string;
+ stroke?: string;
+}
+
+export default function Shangjia(props: IconProps) {
+ const root = useRef(null)
+ const { size = '1em', width, height, spin, rtl, color, fill, stroke, className, ...rest } = props;
+ const _width = width || size;
+ const _height = height || size;
+ const _stroke = stroke || color;
+ const _fill = fill || color;
+ useEffect(() => {
+ if (!_fill) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-fill]').forEach(item => {
+ item.setAttribute('fill', item.getAttribute('data-follow-fill') || '')
+ })
+ }
+ if (!_stroke) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-stroke]').forEach(item => {
+ item.setAttribute('stroke', item.getAttribute('data-follow-stroke') || '')
+ })
+ }
+ }, [stroke, color, fill])
+ return (
+
+
+
+ )
+}
diff --git a/ymir/web/src/components/icon/Xiangmudiedai.tsx b/ymir/web/src/components/icon/Xiangmudiedai.tsx
new file mode 100644
index 0000000000..017a643fa2
--- /dev/null
+++ b/ymir/web/src/components/icon/Xiangmudiedai.tsx
@@ -0,0 +1,49 @@
+import React, { useEffect, useRef } from 'react';
+import styles from './style.css';
+interface IconProps extends React.SVGProps {
+ size?: string | number;
+ width?: string | number;
+ height?: string | number;
+ spin?: boolean;
+ rtl?: boolean;
+ color?: string;
+ fill?: string;
+ stroke?: string;
+}
+
+export default function Xiangmudiedai(props: IconProps) {
+ const root = useRef(null)
+ const { size = '1em', width, height, spin, rtl, color, fill, stroke, className, ...rest } = props;
+ const _width = width || size;
+ const _height = height || size;
+ const _stroke = stroke || color;
+ const _fill = fill || color;
+ useEffect(() => {
+ if (!_fill) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-fill]').forEach(item => {
+ item.setAttribute('fill', item.getAttribute('data-follow-fill') || '')
+ })
+ }
+ if (!_stroke) {
+ (root.current as SVGSVGElement)?.querySelectorAll('[data-follow-stroke]').forEach(item => {
+ item.setAttribute('stroke', item.getAttribute('data-follow-stroke') || '')
+ })
+ }
+ }, [stroke, color, fill])
+ return (
+
+
+
+ )
+}
diff --git a/ymir/web/src/components/icon/style.css b/ymir/web/src/components/icon/style.css
new file mode 100644
index 0000000000..2da8ab39c6
--- /dev/null
+++ b/ymir/web/src/components/icon/style.css
@@ -0,0 +1,9 @@
+.spin,.spin svg {animation: iconpark-spin 1s infinite linear;}
+.rtl,.rtl svg {transform: scaleX(-1);}
+.spin.rtl,.spin.rtl svg {animation: iconpark-spin-rtl 1s infinite linear;}
+@keyframes iconpark-spin {
+ 0% { -webkit-transform: rotate(0); transform: rotate(0);} 100% {-webkit-transform: rotate(360deg); transform: rotate(360deg);}
+}
+@keyframes iconpark-spin-rtl {
+ 0% {-webkit-transform: scaleX(-1) rotate(0); transform: scaleX(-1) rotate(0);} 100% {-webkit-transform: scaleX(-1) rotate(360deg); transform: scaleX(-1) rotate(360deg);}
+}
\ No newline at end of file
diff --git a/ymir/web/src/components/model/editStageCell.js b/ymir/web/src/components/model/editStageCell.js
new file mode 100644
index 0000000000..2e0b330be0
--- /dev/null
+++ b/ymir/web/src/components/model/editStageCell.js
@@ -0,0 +1,72 @@
+import { useState, useEffect, useRef } from "react"
+import { Col, Form, Input, Row, Select } from "antd"
+import t from '@/utils/t'
+import { percent } from '@/utils/number'
+import useFetch from '@/hooks/useFetch'
+const { useForm } = Form
+
+const EditStageCell = ({ record, saveHandle = () => { } }) => {
+ const [editing, setEditing] = useState(false)
+ const selectRef = useRef(null)
+ const [form] = useForm()
+ const [result, setRecommendStage] = useFetch('model/setRecommendStage')
+ const recommendStage = record.stages.find(stage => stage.id === record.recommendStage) || {}
+ const multipleStages = record.stages.length > 1
+
+ useEffect(() => {
+ if (editing && multipleStages) {
+ selectRef.current.focus()
+ }
+ }, [editing])
+
+ useEffect(() => {
+ if (result) {
+ saveHandle(result, record)
+ }
+ }, [result])
+
+ const save = async () => {
+ try {
+ const values = await form.validateFields()
+ await setRecommendStage({ ...values, model: record.id })
+ } catch (errInfo) {
+ console.log('Save failed:', errInfo)
+ }
+ setEditing(false)
+ }
+
+ const tagRender = ({ stage, color = 'rgba(0, 0, 0, 0.65)' }) => (
+ {stage.name}
+ mAP: {percent(stage.map)}
+
)
+
+ return editing && multipleStages ? (
+
+ setEditing(false)}
+ onChange={save}
+ options={record?.stages?.map(stage => ({ stage, label: tagRender({ stage }), value: stage.id }))}
+ tagRender={tagRender}
+ >
+
+
+ ) : (
+ setEditing(true)}
+ style={{ cursor: multipleStages ? 'pointer' : 'text' }}
+ >
+ {tagRender({ stage: recommendStage, color: 'orange' })}
+
+ )
+}
+
+export default EditStageCell
diff --git a/ymir/web/src/components/model/list.js b/ymir/web/src/components/model/list.js
index f02e53a07e..75d8face3a 100644
--- a/ymir/web/src/components/model/list.js
+++ b/ymir/web/src/components/model/list.js
@@ -2,17 +2,16 @@ import React, { useEffect, useState, useRef } from "react"
import { connect } from 'dva'
import styles from "./list.less"
import { Link, useHistory } from "umi"
-import { Form, Input, Table, Modal, Row, Col, Tooltip, Pagination, Space, Empty, Button, } from "antd"
-import {
- SearchOutlined,
-} from "@ant-design/icons"
+import { Form, Input, Table, Modal, Row, Col, Tooltip, Pagination, Space, Empty, Button, message, Popover, } from "antd"
import { diffTime } from '@/utils/date'
-import { states } from '@/constants/model'
+import { ResultStates } from '@/constants/common'
import { TASKTYPES, TASKSTATES } from '@/constants/task'
import t from "@/utils/t"
-import { percent } from '@/utils/number'
+import usePublish from "@/hooks/usePublish"
+import { getDeployUrl } from '@/constants/common'
+import CheckProjectDirty from "@/components/common/CheckProjectDirty"
import Actions from "@/components/table/actions"
import TypeTag from "@/components/task/typeTag"
import RenderProgress from "@/components/common/progress"
@@ -23,26 +22,32 @@ import { getTensorboardLink } from "@/services/common"
import {
ShieldIcon, VectorIcon, EditIcon,
- EyeOffIcon, DeleteIcon, FileDownloadIcon, TrainIcon, WajueIcon, StopIcon,SearchIcon,
+ EyeOffIcon, DeleteIcon, FileDownloadIcon, TrainIcon, WajueIcon, StopIcon, SearchIcon,
ArrowDownIcon, ArrowRightIcon, ImportIcon, BarchartIcon
} from "@/components/common/icons"
+import EditStageCell from "./editStageCell"
+import { DescPop } from "../common/descPop"
+import useRerunAction from "../../hooks/useRerunAction"
const { useForm } = Form
-function Model({ pid, project = {}, iterations, group, modelList, versions, query, ...func }) {
+function Model({ pid, project = {}, iterations, groups, modelList, versions, query, ...func }) {
const history = useHistory()
const { name } = history.location.query
const [models, setModels] = useState([])
const [modelVersions, setModelVersions] = useState({})
const [total, setTotal] = useState(1)
- const [selectedVersions, setSelectedVersions] = useState({})
+ const [selectedVersions, setSelectedVersions] = useState({ selected: [], versions: {} })
const [form] = useForm()
const [current, setCurrent] = useState({})
- const [visibles, setVisibles] = useState(group ? { [group]: true } : {})
+ const [visibles, setVisibles] = useState({})
+ const [trainingUrl, setTrainingUrl] = useState('')
let [lock, setLock] = useState(true)
const hideRef = useRef(null)
const delGroupRef = useRef(null)
const terminateRef = useRef(null)
+ const generateRerun = useRerunAction()
+ const [publish, publishResult] = usePublish()
/** use effect must put on the top */
useEffect(() => {
@@ -52,6 +57,11 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
setLock(false)
}, [history.location])
+ useEffect(() => {
+ const initVisibles = groups.reduce((prev, group) => ({ ...prev, [group]: true }), {})
+ setVisibles(initVisibles)
+ }, [groups])
+
useEffect(() => {
const mds = setGroupLabelsByProject(modelList.items, project)
setModels(mds)
@@ -106,6 +116,14 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
}
}, [query, lock])
+ useEffect(() => {
+ const selected = selectedVersions.selected
+ const mvs = Object.values(modelVersions).flat().filter(version => selected.includes(version.id))
+ const hashs = mvs.map(version => version.task.hash)
+ const url = getTensorboardLink(hashs)
+ setTrainingUrl(url)
+ }, [selectedVersions])
+
async function initState() {
await func.resetQuery()
form.resetFields()
@@ -116,13 +134,19 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
title: showTitle("model.column.name"),
dataIndex: "versionName",
className: styles[`column_name`],
- render: (name, { id, state, projectLabel, iterationLabel }) =>
- {name}
-
- {projectLabel ? {projectLabel}
: null}
- {iterationLabel ? {iterationLabel}
: null}
-
-
,
+ render: (name, { id, description, projectLabel, iterationLabel }) => {
+ const popContent =
+ const content =
+ {name}
+
+ {projectLabel ? {projectLabel}
: null}
+ {iterationLabel ? {iterationLabel}
: null}
+
+
+ return description ?
+ {content}
+ : content
+ },
ellipsis: true,
},
{
@@ -131,16 +155,17 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
render: (type) => ,
},
{
- title: showTitle("model.column.map"),
- dataIndex: "map",
- render: map => {percent(map)} ,
- sorter: (a, b) => a - b,
- align: 'center',
+ title: showTitle("model.column.stage"),
+ dataIndex: "recommendStage",
+ render: (_, record) => isValidModel(record.state) ?
+ : null,
+ // align: 'center',
+ width: 300,
},
{
title: showTitle('dataset.column.state'),
dataIndex: 'state',
- render: (state, record) => RenderProgress(state, record, true),
+ render: (state, record) => RenderProgress(state, record),
// width: 60,
},
{
@@ -169,9 +194,19 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
const listChange = (current, pageSize) => {
const limit = pageSize
const offset = (current - 1) * pageSize
- func.updateQuery({ ...query, limit, offset })
+ func.updateQuery({ ...query, current, limit, offset })
}
+ function updateModelVersion(result) {
+ setModelVersions(mvs => {
+ return {
+ ...mvs,
+ [result.groupId]: mvs[result.groupId].map(version => {
+ return version.id === result.id ? result : version
+ })
+ }
+ })
+ }
async function showVersions(id) {
setVisibles((old) => ({ ...old, [id]: !old[id] }))
@@ -182,6 +217,7 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
Object.keys(versions).forEach(gid => {
const list = versions[gid]
const updatedList = list.map(item => {
+ delete item.iterationLabel
const iteration = iterations.find(iter => iter.model === item.id)
if (iteration) {
item.iterationLabel = t('iteration.tag.round', iteration)
@@ -197,6 +233,7 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
Object.keys(versions).forEach(gid => {
const list = versions[gid]
const updatedList = list.map(item => {
+ delete item.projectLabel
item = setLabelByProject(project?.model, 'isInitModel', item)
return { ...item }
})
@@ -207,6 +244,7 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
function setGroupLabelsByProject(items, project) {
return items.map(item => {
+ delete item.projectLabel
item = setLabelByProject(project?.model, 'isInitModel', item)
return { ...item }
})
@@ -234,8 +272,16 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
}
const actionMenus = (record) => {
- const { id, name, url, state, taskState, taskType, task, isProtected } = record
+ const { id, name, url, state, taskState, taskType, task, isProtected, stages, recommendStage } = record
+
const actions = [
+ {
+ key: "publish",
+ label: t("model.action.publish"),
+ hidden: () => !isValidModel(state) || !getDeployUrl(),
+ onclick: () => publish(record),
+ icon: ,
+ },
{
key: "verify",
label: t("model.action.verify"),
@@ -255,21 +301,21 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
key: "mining",
label: t("dataset.action.mining"),
hidden: () => !isValidModel(state),
- onclick: () => history.push(`/home/task/mining/${pid}?mid=${id}`),
+ onclick: () => history.push(`/home/project/${pid}/mining?mid=${id},${recommendStage}`),
icon: ,
},
{
key: "train",
label: t("dataset.action.train"),
hidden: () => !isValidModel(state),
- onclick: () => history.push(`/home/task/train/${pid}?mid=${id}`),
+ onclick: () => history.push(`/home/project/${pid}/train?mid=${id},${recommendStage}`),
icon: ,
},
{
key: "inference",
label: t("dataset.action.inference"),
hidden: () => !isValidModel(state),
- onclick: () => history.push(`/home/task/inference/${pid}?mid=${id}`),
+ onclick: () => history.push(`/home/project/${pid}/inference?mid=${id},${recommendStage}`),
icon: ,
},
{
@@ -281,17 +327,18 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
},
{
key: "tensor",
- label: 'Tensorboard',
+ label: t('task.action.training'),
target: '_blank',
link: getTensorboardLink(task.hash),
hidden: () => taskType !== TASKTYPES.TRAINING,
icon: ,
},
+ generateRerun(record),
{
key: "hide",
label: t("common.action.hide"),
onclick: () => hide(record),
- hidden: ()=> hideHidden(record),
+ hidden: () => hideHidden(record),
icon: ,
},
]
@@ -304,15 +351,32 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
}
const multipleInfer = () => {
- const ids = Object.values(selectedVersions)
- history.push(`/home/task/inference/${pid}?mid=${ids}`)
+ const { selected } = selectedVersions
+ const versionsObject = Object.values(versions).flat()
+ const stages = versionsObject.filter(md => selected.includes(md.id)).map(md => {
+ return [md.id, md.recommendStage].toString()
+ })
+ if (stages.length) {
+ history.push(`/home/project/${pid}/inference?mid=${stages.join('|')}`)
+ } else {
+ message.warning(t('model.list.batch.invalid'))
+ }
}
const multipleHide = () => {
- const ids = Object.values(selectedVersions).flat()
const allVss = Object.values(versions).flat()
- const vss = allVss.filter(({id}) => ids.includes(id))
- hideRef.current.hide(vss, project.hiddenModels)
+ const vss = allVss.filter(({ id, state }) => selectedVersions.selected.includes(id))
+ if (vss.length) {
+ hideRef.current.hide(vss, project.hiddenModels)
+ } else {
+ message.warning(t('model.list.batch.invalid'))
+ }
+ }
+
+ const getDisabledStatus = (filter = () => { }) => {
+ const allVss = Object.values(versions).flat()
+ const { selected } = selectedVersions
+ return !selected.length || allVss.filter(({ id }) => selected.includes(id)).some(version => filter(version))
}
const hide = (version) => {
@@ -325,12 +389,18 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
const hideOk = (result) => {
result.forEach(item => fetchVersions(item.model_group_id, true))
getData()
- setSelectedVersions({})
+ setSelectedVersions({ selected: [], versions: {} })
}
-
+
function rowSelectChange(gid, rowKeys) {
- setSelectedVersions(old => ({ ...old, [gid]: rowKeys }))
+ setSelectedVersions(({ versions }) => {
+ versions[gid] = rowKeys
+ return {
+ selected: Object.values(versions).flat(),
+ versions: { ...versions },
+ }
+ })
}
const stop = (dataset) => {
@@ -369,15 +439,15 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
}
function isValidModel(state) {
- return states.VALID === state
+ return ResultStates.VALID === state
}
function isRunning(state) {
- return states.READY === state
+ return ResultStates.READY === state
}
function add() {
- history.push(`/home/model/import/${pid}`)
+ history.push(`/home/project/${pid}/model/import`)
}
const addBtn = (
@@ -386,16 +456,17 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
)
- const renderMultipleActions = Object.values(selectedVersions).flat().length ? (
- <>
-
- {t("common.action.multiple.hide")}
-
-
- {t("common.action.multiple.infer")}
-
- >
- ) : null
+ const renderMultipleActions = <>
+ isRunning(state))} onClick={multipleHide}>
+ {t("common.action.multiple.hide")}
+
+ !isValidModel(state))} onClick={multipleInfer}>
+ {t("common.action.multiple.infer")}
+
+
+ {t('task.action.training.batch')}
+
+ >
const renderGroups = (<>
@@ -416,8 +487,8 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
onChange={tableChange}
rowKey={(record) => record.id}
rowSelection={{
+ selectedRowKeys: selectedVersions.versions[group.id],
onChange: (keys) => rowSelectChange(group.id, keys),
- getCheckboxProps: (record) => ({ disabled: isRunning(record.state), }),
}}
rowClassName={(record, index) => index % 2 === 0 ? '' : 'oddRow'}
columns={columns}
@@ -426,17 +497,23 @@ function Model({ pid, project = {}, iterations, group, modelList, versions, quer
) : }
-
+
>)
return (
-
-
- {addBtn}
- {renderMultipleActions}
-
-
+
+
+
+ {addBtn}
+ {renderMultipleActions}
+
+
+
+
+
+
-
+ {item.enableIteration ?
{t('project.iteration.number')}
{item.round}
-
+ : null }
{t('project.content.desc')}: {item.description}
@@ -63,7 +64,7 @@ export const Lists = ({ projects = [], more = '' }) => {
>
return { history.push(`/home/project/detail/${item.id}`) }}>
+ onClick={() => { history.push(`/home/project/${item.id}/detail`) }}>
diff --git a/ymir/web/src/components/table/actions.js b/ymir/web/src/components/table/actions.js
index b22b8e7928..e584c26450 100644
--- a/ymir/web/src/components/table/actions.js
+++ b/ymir/web/src/components/table/actions.js
@@ -6,25 +6,29 @@ const actions = (menus) => menus.map((menu, i) => action(menu, i === menus.lengt
const isOuterLink = (link) => /^http(s)?:/i.test(link)
-const moreActions = (menus) => {
- return (
-
- {menus.map((menu) => (
-
- {menu.link ? {action(menu)} : action(menu)}
-
- ))}
-
- )
-}
+const moreActions = (menus) => ({
+ key: menu.key,
+ label: action(menu)
+ }))} />
function action({ key, onclick = () => { }, icon, label, link, target, disabled }, last) {
+ const cls = `${s.action} ${last ? s.last : ''}`
const btn = (
-
+
{icon}{label}
)
- return link ? {btn} : btn
+ return link ?
+ {icon} {label}
+ : btn
}
const Actions = ({ menus, showCount = 3 }) => {
diff --git a/ymir/web/src/components/table/table.less b/ymir/web/src/components/table/table.less
index 43a0b0ea14..b8cdabb50d 100644
--- a/ymir/web/src/components/table/table.less
+++ b/ymir/web/src/components/table/table.less
@@ -1,6 +1,7 @@
.actions {
color: #3BA0FF;
.action {
+ white-space: nowrap;
padding: 0;
height: 22px;
line-height: 22px;
@@ -23,7 +24,9 @@
margin-left: 0;
}
}
- .l {
-
+}
+.more {
+ .action {
+ color: rgba(0, 0, 0, 0.65);
}
}
\ No newline at end of file
diff --git a/ymir/web/src/components/tabs/cardTabs.js b/ymir/web/src/components/tabs/cardTabs.js
new file mode 100644
index 0000000000..2402eb5255
--- /dev/null
+++ b/ymir/web/src/components/tabs/cardTabs.js
@@ -0,0 +1,37 @@
+import { Card } from "antd"
+import { useEffect, useState } from "react"
+import { useLocation, useHistory } from "umi"
+
+export const CardTabs = ({ data = [], initialTab, ...props }) => {
+ const location = useLocation()
+ const history = useHistory()
+ const [tabs, setTabs] = useState([])
+ const [contents, setContents] = useState({})
+ const [active, setActive] = useState(null)
+
+ useEffect(() => (tabs.length && !active) && setActive(initialTab || tabs[0].key), [tabs])
+ useEffect(() => {
+ const type = location?.state?.type
+ if (typeof type !== 'undefined') {
+ setActive(type)
+ }
+ }, [location.state])
+
+ useEffect(() => {
+ setTabs(data.map(({ tab, key }) => ({ tab: tab || key, key })))
+ setContents(data.reduce((prev, { content, key }) => ({ ...prev, [key]: content }), {}))
+ }, [data])
+
+ return history.replace({ state: { type: key } })}
+ tabProps={{
+ moreIcon: null,
+ }}
+ >
+ {contents[active]}
+
+}
diff --git a/ymir/web/src/components/task/BottomButtons.js b/ymir/web/src/components/task/BottomButtons.js
new file mode 100644
index 0000000000..8e8b7d5709
--- /dev/null
+++ b/ymir/web/src/components/task/BottomButtons.js
@@ -0,0 +1,71 @@
+import React, { } from "react"
+import { Button, Form, Space } from "antd"
+
+import t from "@/utils/t"
+
+
+function BottomButtons({
+ mode='normal',
+ fromIteration = false,
+ stage,
+ result,
+ stepState = 'start',
+ label = 'common.confirm',
+ ok = () => { },
+ next = () => {},
+ skip = () => { },
+ update = () => { }
+}) {
+
+ const currentStage = () => stage.value === stage.current
+ const finishStage = () => stage.value < stage.current
+ const pendingStage = () => stage.value > stage.current
+
+ const isPending = () => state < 0
+ const isReady = () => state === states.READY
+ const isValid = () => state === states.VALID
+ const isInvalid = () => state === states.INVALID
+
+ // !stage.unskippable && !end && currentStage()
+ const skipBtn =
+
+ {t('common.skip')}
+
+
+
+ const confirmBtn =
+
+ {t(label)}
+
+
+
+ const backBtn =
+ history.goBack()}>
+ {t('common.step.next')}
+
+
+
+ const nextBtn =
+
+ {t('task.btn.back')}
+
+
+
+ return (
+
+
+ {mode === 'iteration' ? <>
+ {stepState === 'start' ? confirmBtn : null}
+ {stepState === 'finish' ? nextBtn: null}
+ {skipBtn}
+ > : <>
+ {confirmBtn}
+ {backBtn}
+ >
+ }
+
+
+ )
+}
+
+export default BottomButtons
diff --git a/ymir/web/src/components/task/detail.js b/ymir/web/src/components/task/detail.js
index 9e739cffc0..e22f67e65a 100644
--- a/ymir/web/src/components/task/detail.js
+++ b/ymir/web/src/components/task/detail.js
@@ -1,12 +1,8 @@
-import React, { useEffect, useRef, useState } from "react"
-import { connect } from "dva"
-import { Link, useHistory } from "umi"
+import React, { useEffect, useState } from "react"
+import { Link, useHistory, useParams } from "umi"
import {
- Button,
- Card,
Col,
Descriptions,
- Progress,
Row,
Space,
Tag,
@@ -15,24 +11,36 @@ import {
import t from "@/utils/t"
import { format } from "@/utils/date"
import { getTensorboardLink } from "@/services/common"
-import Terminate from "./terminate"
-import { TASKTYPES } from "@/constants/task"
-import s from "./detail.less"
-import IgnoreKeywords from "../common/ignoreKeywords"
+import { TASKTYPES, getTaskTypeLabel } from "@/constants/task"
+import useFetch from '@/hooks/useFetch'
+import { getRecommendStage } from '@/constants/model'
+
+import renderLiveCodeItem from '@/components/task/items/livecode'
const { Item } = Descriptions
-function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
+function TaskDetail({ task = {} }) {
const history = useHistory()
const id = task.id
- const [datasets, setDatasets] = useState({})
- const [model, setModel] = useState({})
+ const { id: pid } = useParams()
+ const [datasetNames, setDatasetNames] = useState({})
+ const [datasets, getDatasets] = useFetch('dataset/batchDatasets', [])
+ const [model, getModel] = useFetch('model/getModel', {})
useEffect(() => {
task.id && !isImport(task.type) && fetchDatasets()
- hasValidModel(task.type) && task?.parameters?.model_id && fetchModel(task.parameters.model_id)
+ hasValidModel(task.type) && task?.parameters?.model_id && getModel({ id: task.parameters.model_id })
}, [task.id])
+ useEffect(() => {
+ if (!datasets.length) {
+ return
+ }
+ const names = {}
+ datasets.forEach((ds) => (names[ds.id] = ds))
+ setDatasetNames(names)
+ }, [datasets])
+
async function fetchDatasets() {
const pa = task.parameters || {}
const inds = pa.include_datasets || []
@@ -47,16 +55,7 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
if (!ids.length) {
return
}
- const dss = await batchDatasets(ids)
- const names = {}
- dss.forEach((ds) => (names[ds.id] = ds))
- setDatasets(names)
- }
-
- async function fetchModel(id) {
- const result = await getModel(id)
-
- result && setModel(result)
+ getDatasets({ pid, ids })
}
const labelStyle = {
@@ -65,9 +64,6 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
justifyContent: "flex-end",
}
- function isModel(type) {
- return [TASKTYPES.TRAINING, TASKTYPES.MODELCOPY, TASKTYPES.MODELIMPORT].includes(type)
- }
function hasValidModel(type) {
return [TASKTYPES.TRAINING, TASKTYPES.MINING, TASKTYPES.INFERENCE].includes(type)
}
@@ -77,7 +73,7 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
}
function renderDatasetName(id) {
- const ds = datasets[id]
+ const ds = datasetNames[id]
const name = ds ? `${ds.name} ${ds.versionName}` : id
return (
@@ -89,23 +85,49 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
return {dts.map((id) => renderDatasetName(id))}
}
- function renderConfig(config = {}) {
- return Object.keys(config).map((key) => (
-
-
- {key}:
-
- {config[key].toString()}
-
- ))
+ function renderModel(id, pid, model = {}, label = 'task.mining.form.model.label') {
+ const name = model.id ? `${model.name} ${model.versionName} ${getRecommendStage(model).name}` : id
+ return id ? -
+
+ {name}
+
+ : null
}
- function renderTrainKeywords(keywords = []) {
- return -
- {keywords.map((keyword) => (
-
{keyword}
+ function renderDuration(label) {
+ return label ? - {label}
: null
+ }
+
+ function renderKeepAnnotations(type) {
+ const maps = { 1: 'gt', 2: 'pred' }
+ const label = type ? maps[type] : 'none'
+ return t(`task.label.form.keep_anno.${label}`)
+ }
+
+ function renderPreProcess(preprocess) {
+ return preprocess ? -
+ {Object.keys(preprocess).map((key) => (
+
+
+ {key}:
+
+ {JSON.stringify(preprocess[key])}
+
))}
-
+ : null
+ }
+
+ function renderConfig(config = {}) {
+ return - {
+ Object.keys(config).map((key) => (
+
+
+ {key}:
+
+ {config[key].toString()}
+
+ ))
+ }
}
function renderTrainImage(image, span = 1) {
@@ -114,20 +136,6 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
}
- function renderTrainAlgo(param = {}) {
- return <>
- -
- {param.network}
-
- -
- {param.backbone}
-
- -
- {t('task.train.form.traintypes.detect')}
-
- >
- }
-
function renderDatasetSource(id) {
return - {renderDatasetName(id)}
}
@@ -151,6 +159,8 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
[TASKTYPES.COPY]: renderCopy,
[TASKTYPES.INFERENCE]: renderInference,
[TASKTYPES.FUSION]: renderFusion,
+ [TASKTYPES.MERGE]: renderMerge,
+ [TASKTYPES.FILTER]: renderFilter,
[TASKTYPES.MODELCOPY]: renderModelCopy,
[TASKTYPES.MODELIMPORT]: renderModelImport,
[TASKTYPES.SYS]: renderSys,
@@ -168,28 +178,24 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
-
{renderDatasetName(task.parameters.validation_dataset_id)}
- {renderTrainKeywords(task?.parameters?.keywords)}
- {renderTrainAlgo(task?.parameters)}
+ {renderModel(task.parameters.model_id, task.project_id, model, 'task.detail.label.premodel')}
+ {renderDuration(task.durationLabel)}
+ {renderLiveCodeItem(task.config)}
{renderTrainImage(task?.parameters?.docker_image, 2)}
- -
+
-
{t("task.detail.tensorboard.link.label")}
- -
- {renderConfig(task.config)}
-
+ {renderPreProcess(task.parameters?.preprocess)}
+ {renderConfig(task.config)}
>
)
const renderMining = () => (
<>
{renderDatasetSource(task?.parameters.dataset_id)}
{renderCreateTime(task.create_datetime)}
- -
-
- {model?.name || task.parameters.model_id}
-
-
+ {renderModel(task.parameters.model_id, task.project_id, model)}
-
{task.parameters.mining_algorithm}
@@ -204,9 +210,8 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
-
{task.parameters.docker_image}
- -
- {renderConfig(task.config)}
-
+ {renderLiveCodeItem(task.config)}
+ {renderConfig(task.config)}
>
)
const renderLabel = () => (
@@ -224,7 +229,8 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
))}
-
- {task.parameters.keep_annotations ? t("common.yes") : t("common.no")}
+ {renderKeepAnnotations(task.parameters.annotation_type)}
+ {/* {task.parameters.keep_annotations ? t("common.yes") : t("common.no")} */}
-
{task.parameters.extra_url ? (
@@ -238,39 +244,28 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
const renderModelImport = () => <>
- {t('task.type.modelimport')}
{renderCreateTime(task.create_datetime)}
- {renderTrainKeywords(task?.parameters?.keywords)}
- {renderTrainAlgo(task?.parameters)}
{renderTrainImage(task?.parameters?.docker_image, 2)}
- -
- {renderConfig(task.config)}
-
+ {renderConfig(task.config)}
>
const renderModelCopy = () => <>
- {t('task.type.modelcopy')}
{renderCreateTime(task.create_datetime)}
- {renderTrainKeywords(task?.parameters?.keywords)}
- {renderTrainAlgo(task?.parameters)}
+ {renderModel(task.parameters.model_id, task.project_id, model, 'task.detail.label.premodel')}
+ {renderLiveCodeItem(task.config)}
{renderTrainImage(task?.parameters?.docker_image, 2)}
- -
- {renderConfig(task.config)}
-
+ {renderPreProcess(task.parameters?.preprocess)}
+ {renderConfig(task.config)}
>
const renderImport = () => (
<>
{renderImportSource(task?.parameters)}
{renderCreateTime(task.create_datetime)}
- -
-
-
>
)
const renderCopy = () => (
<>
{renderImportSource(task?.parameters)}
{renderCreateTime(task.create_datetime)}
- -
-
-
>
)
const renderInference = () => (
@@ -288,12 +283,11 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
-
{task.parameters.top_k}
- -
+
-
{task.parameters.docker_image}
- -
- {renderConfig(task.config)}
-
+ {renderLiveCodeItem(task.config)}
+ {renderConfig(task.config)}
>
)
const renderFusion = () => (
@@ -321,6 +315,37 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
>
)
+ const renderMerge = () => (
+ <>
+ {renderDatasetSource(task?.parameters?.dataset_id)}
+ {renderCreateTime(task.create_datetime)}
+ -
+ {renderDatasetNames(task?.parameters?.include_datasets)}
+
+ -
+ {renderDatasetNames(task?.parameters?.exclude_datasets)}
+
+ >
+ )
+ const renderFilter = () => (
+ <>
+ {renderDatasetSource(task?.parameters?.dataset_id)}
+ {renderCreateTime(task.create_datetime)}
+ -
+ {task.parameters?.include_keywords?.map((keyword) => (
+
{keyword}
+ ))}
+
+ -
+ {task.parameters?.exclude_keywords?.map((keyword) => (
+
{keyword}
+ ))}
+
+ -
+ {task?.parameters?.sampling_count}
+
+ >
+ )
return (
@@ -328,7 +353,7 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
column={2}
bordered
labelStyle={labelStyle}
- title={
{t("dataset.column.source")}
}
+ title={
{t("dataset.column.source") + " > " + t(getTaskTypeLabel(task.type))}
}
className='infoTable'
>
{task.id ? renderTypes() : null}
@@ -337,28 +362,4 @@ function TaskDetail({ task = {}, ignore = [], batchDatasets, getModel }) {
)
}
-const props = (state) => {
- return {
- logined: state.user.logined,
- taskItem: state.task.task,
- }
-}
-
-const actions = (dispatch) => {
- return {
- batchDatasets(ids) {
- return dispatch({
- type: "dataset/batchDatasets",
- payload: ids,
- })
- },
- getModel(id, force) {
- return dispatch({
- type: "model/getModel",
- payload: { id, force },
- })
- },
- }
-}
-
-export default connect(props, actions)(TaskDetail)
+export default TaskDetail
diff --git a/ymir/web/src/components/task/error.js b/ymir/web/src/components/task/error.js
index 0e23c3f35f..16f28f59e5 100644
--- a/ymir/web/src/components/task/error.js
+++ b/ymir/web/src/components/task/error.js
@@ -13,7 +13,7 @@ const labelStyle = {
justifyContent: "flex-end",
}
-export default function Error({ code, msg = '' }) {
+export default function Error({ code, msg = '', terminated }) {
const [visible, setVisible] = useState(false)
function formatErrorMessage(message) {
@@ -22,22 +22,29 @@ export default function Error({ code, msg = '' }) {
}
- return
+ const renderError = <>
+
-
+ {code ? t(`error${code}`) : null}
+ {msg ?
setVisible(!visible)}>{visible ? : } : null}
+
+ {msg && visible ?
-
+ {formatErrorMessage(msg)}
+
: null}
+ >
+ const renderTerminated =
-
+ {t('task.detail.terminated')}
+
+
+ return terminated || code ?
{t("task.detail.error.title")}
}
+ title={
{t("task.state")}
}
className='infoTable'
>
-
-
- {t(`error${code}`)}
- {msg ?
setVisible(!visible)}>{visible ? : } : null}
-
- {msg && visible ?
-
- {formatErrorMessage(msg)}
-
: null}
+ {terminated ? renderTerminated : renderError }
-
+
: null
}
\ No newline at end of file
diff --git a/ymir/web/src/components/task/fusion.js b/ymir/web/src/components/task/fusion.js
new file mode 100644
index 0000000000..f5ac278708
--- /dev/null
+++ b/ymir/web/src/components/task/fusion.js
@@ -0,0 +1,233 @@
+import React, { useState, useEffect, useCallback } from "react"
+import { Select, Button, Form, message, Card, Space, Radio, Row, Col, InputNumber, Checkbox } from "antd"
+import { useHistory, useParams, useSelector } from "umi"
+
+import { formLayout } from "@/config/antd"
+import t from "@/utils/t"
+import { randomNumber } from "@/utils/number"
+import useFetch from '@/hooks/useFetch'
+import { MiningStrategy } from '@/constants/iteration'
+
+import RecommendKeywords from "@/components/common/recommendKeywords"
+import Panel from "@/components/form/panel"
+import DatasetSelect from "@/components/form/datasetSelect"
+import Desc from "@/components/form/desc"
+import SubmitButtons from "./submitButtons"
+
+function Fusion({ query = {}, hidden, ok = () => { }, bottom }) {
+ const { did, iterationId, currentStage, chunk, strategy = '', merging } = query
+
+ const pageParams = useParams()
+ const pid = Number(pageParams.id)
+ const history = useHistory()
+ const [form] = Form.useForm()
+ const [includeDatasets, setIncludeDatasets] = useState([])
+ const [excludeDatasets, setExcludeDatasets] = useState([])
+ const [miningStrategy, setMiningStrategy] = useState(strategy || 0)
+ const [excludeResult, setExcludeResult] = useState(strategy === '' ? false : true)
+ const [keywords, setKeywords] = useState([])
+ const [selectedKeywords, setSelectedKeywords] = useState([])
+ const [selectedExcludeKeywords, setExcludeKeywords] = useState([])
+ const [visible, setVisible] = useState(false)
+ const [fusionResult, fusion] = useFetch("task/fusion")
+ const dataset = useSelector(({ dataset }) => dataset.dataset[did] || {})
+ const [_d, getDataset] = useFetch('dataset/getDataset')
+
+ const initialValues = {
+ name: 'task_fusion_' + randomNumber(),
+ samples: chunk || 1000,
+ include_datasets: Number(merging) ? [Number(merging)] : [],
+ strategy: 2,
+ }
+
+ useEffect(() => fusionResult && ok(fusionResult), [fusionResult])
+
+ useEffect(() => did && getDataset({ id: did }), [did])
+
+ useEffect(() => {
+ dataset.id && includeDatasets.length && setKeywordOptions([dataset, ...includeDatasets])
+ }, [dataset.id, includeDatasets])
+
+ useEffect(() => {
+ const state = history.location.state
+
+ if (state?.record) {
+ const { parameters, name, } = state.record
+ const { include_classes, include_datasets, exclude_classes, include_strategy } = parameters
+ //do somethin
+ form.setFieldsValue({
+ name: `${name}_${randomNumber()}`,
+ datasets: include_datasets,
+ inc: include_classes,
+ exc: exclude_classes,
+ strategy: include_strategy,
+ })
+ setSelectedKeywords(include_classes)
+ setExcludeKeywords(exclude_classes)
+ history.replace({ state: {} })
+ }
+ }, [history.location.state])
+
+ const setKeywordOptions = (datasets = []) => {
+ const kws = datasets.map(ds => ds.keywords).flat().filter(i => i)
+ console.log('kws:', kws)
+ setKeywords([...new Set(kws)].sort())
+ }
+
+ const checkInputs = (i) => {
+ return i.exc || i.inc || i.samples || i?.exclude_datasets?.length || i?.include_datasets?.length
+ }
+
+ const onFinish = async (values) => {
+ if (!checkInputs(values)) {
+ return message.error(t('dataset.fusion.validate.inputs'))
+ }
+ const params = {
+ ...values,
+ project_id: dataset.projectId,
+ group_id: dataset.groupId,
+ dataset: did,
+ include: selectedKeywords,
+ exclude: selectedExcludeKeywords,
+ mining_strategy: miningStrategy,
+ exclude_result: excludeResult,
+ include_strategy: Number(values.strategy) || 2,
+ }
+ if (iterationId) {
+ params.iteration = iterationId
+ params.stage = currentStage
+ }
+ fusion(params)
+ }
+
+ const onFinishFailed = (err) => {
+ console.log("on finish failed: ", err)
+ }
+
+ function onIncludeDatasetChange(values) {
+ setIncludeDatasets(values)
+
+ // reset
+ setSelectedKeywords([])
+ setExcludeKeywords([])
+ form.setFieldsValue({ inc: [], exc: [] })
+ }
+ function onExcludeDatasetChange(values) {
+ setExcludeDatasets(values)
+ // todo inter keywords
+ }
+
+ function miningStrategyChanged({ target: { checked } }) {
+ if (Number(strategy) === MiningStrategy.free) {
+ setMiningStrategy(checked ? MiningStrategy.unique : MiningStrategy.free)
+ setExcludeResult(true)
+ } else {
+ setExcludeResult(checked)
+ }
+ }
+
+ function selectRecommendKeywords(keyword) {
+ const kws = [...new Set([...selectedKeywords, keyword])]
+ setSelectedKeywords(kws)
+ form.setFieldsValue({ inc: kws })
+ }
+
+ const includesFilter = useCallback((dss) => dss.filter(ds => ![...excludeDatasets, did].includes(ds.id)), [excludeDatasets, did])
+
+ return (
+
+ {bottom ? bottom : }
+
+
+ )
+}
+
+export default Fusion
diff --git a/ymir/web/src/components/task/items/keywords.js b/ymir/web/src/components/task/items/keywords.js
new file mode 100644
index 0000000000..2c46cd2fd1
--- /dev/null
+++ b/ymir/web/src/components/task/items/keywords.js
@@ -0,0 +1,8 @@
+import { Descriptions, Tag } from "antd"
+import t from '@/utils/t'
+
+export default (keywords = []) => (
+
+ {keywords.map((keyword) => {keyword} )}
+
+)
diff --git a/ymir/web/src/components/task/items/livecode.js b/ymir/web/src/components/task/items/livecode.js
new file mode 100644
index 0000000000..3baf9d42dc
--- /dev/null
+++ b/ymir/web/src/components/task/items/livecode.js
@@ -0,0 +1,22 @@
+import { Descriptions, Tag } from "antd"
+import t from '@/utils/t'
+import { FIELDS, getConfigUrl, isLiveCode } from "@/components/form/items/liveCodeConfig"
+
+export default (config = {}) => {
+ const configUrl = getConfigUrl(config)
+ const fields = FIELDS.map(({ key, field }, index) => ({
+ label: `task.train.live.${key}`,
+ key: field,
+ extra: index === FIELDS.length - 1 ?
{t('common.view')} : null,
+ }))
+ const typeLabel = isLiveCode(config) ? 'live' : 'local'
+ const typeItem =
{t(`task.detail.function.${typeLabel}`)}
+ return <>
+ {typeItem}
+ {isLiveCode(config) ? fields.map(({ label, key, extra }) => (
+
+ {config[key]} {extra}
+
+ )) : null}
+ >
+}
diff --git a/ymir/web/src/components/task/label.js b/ymir/web/src/components/task/label.js
new file mode 100644
index 0000000000..34c66a08b6
--- /dev/null
+++ b/ymir/web/src/components/task/label.js
@@ -0,0 +1,211 @@
+import React, { useEffect, useState } from "react"
+import { connect } from "dva"
+import { Select, Input, Button, Form, Row, Col, Checkbox, Space, } from "antd"
+import { useHistory, useParams, Link } from "umi"
+
+import { formLayout } from "@/config/antd"
+import t from "@/utils/t"
+import Uploader from "@/components/form/uploader"
+import { randomNumber } from "@/utils/number"
+import useFetch from '@/hooks/useFetch'
+
+import DatasetSelect from "@/components/form/datasetSelect"
+import Desc from "@/components/form/desc"
+import Tip from "@/components/form/tip"
+import SubmitButtons from "./submitButtons"
+
+import styles from "./label.less"
+import KeepAnnotations from "./label/keepAnnotations"
+
+const LabelTypes = () => [
+ { id: "part", label: t('task.label.form.type.newer'), checked: true },
+ { id: "all", label: t('task.label.form.type.all') },
+]
+
+function Label({ query = {}, hidden, datasets, keywords, ok = () => { }, bottom, ...func }) {
+ const pageParams = useParams()
+ const pid = Number(pageParams.id)
+ const { iterationId, outputKey, currentStage } = query
+ const did = Number(query.did)
+ const history = useHistory()
+ const [doc, setDoc] = useState(undefined)
+ const [form] = Form.useForm()
+ const [asChecker, setAsChecker] = useState(false)
+ const [project, getProject] = useFetch('project/getProject', {})
+
+ useEffect(() => {
+ func.getKeywords({ limit: 100000 })
+ }, [])
+
+ useEffect(() => {
+ iterationId && pid && getProject({ id: pid })
+ }, [pid, iterationId])
+
+ useEffect(() => {
+ project.id && form.setFieldsValue({ keywords: project.keywords })
+ }, [project])
+
+ const onFinish = async (values) => {
+ const { labellers, checker } = values
+ const emails = [labellers]
+ checker && emails.push(checker)
+ const params = {
+ ...values,
+ projectId: pid,
+ labellers: emails,
+ doc,
+ name: 'task_label_' + randomNumber(),
+ }
+ const result = await func.label(params)
+ result && ok(result.result_dataset)
+ }
+
+ function docChange(files, docFile) {
+ setDoc(files.length ? location.protocol + '//' + location.host + docFile : '')
+ }
+
+ function onFinishFailed(errorInfo) {
+ console.log("Failed:", errorInfo)
+ }
+
+ const getCheckedValue = (list) => list.find((item) => item.checked)["id"]
+ const initialValues = {
+ datasetId: did || undefined,
+ labelType: getCheckedValue(LabelTypes()),
+ }
+ return (
+
+
+ {bottom ? bottom : }
+ {t('task.label.bottomtip', { link: {t('task.label.bottomtip.link.label')} })}
+
+
+
+ )
+}
+
+const dis = (dispatch) => {
+ return {
+ getDataset(id, force) {
+ return dispatch({
+ type: "dataset/getDataset",
+ payload: { id, force },
+ })
+ },
+ label(payload) {
+ return dispatch({
+ type: "task/label",
+ payload,
+ })
+ },
+ clearCache() {
+ return dispatch({ type: "dataset/clearCache", })
+ },
+ getKeywords(payload) {
+ return dispatch({
+ type: 'keyword/getKeywords',
+ payload,
+ })
+ },
+ updateIteration(params) {
+ return dispatch({
+ type: 'iteration/updateIteration',
+ payload: params,
+ })
+ },
+ }
+}
+
+const stat = (state) => {
+ return {
+ datasets: state.dataset.dataset,
+ keywords: state.keyword.keywords.items,
+ }
+}
+
+export default connect(stat, dis)(Label)
diff --git a/ymir/web/src/pages/task/label/index.less b/ymir/web/src/components/task/label.less
similarity index 100%
rename from ymir/web/src/pages/task/label/index.less
rename to ymir/web/src/components/task/label.less
diff --git a/ymir/web/src/components/task/label/keepAnnotations.js b/ymir/web/src/components/task/label/keepAnnotations.js
new file mode 100644
index 0000000000..5d742a95c2
--- /dev/null
+++ b/ymir/web/src/components/task/label/keepAnnotations.js
@@ -0,0 +1,22 @@
+import { Form, Radio } from "antd"
+import t from "@/utils/t"
+
+const options = [
+ { value: 1, label: 'gt' },
+ { value: 2, label: 'pred' },
+ { value: undefined, label: 'none' },
+]
+
+const KeepAnnotations = ({ initialValue, ...rest }) => {
+ const prefix = 'task.label.form.keep_anno.'
+ return
+ ({ ...opt, label: t(prefix + opt.label) }))} />
+
+}
+
+export default KeepAnnotations
diff --git a/ymir/web/src/components/task/merge.js b/ymir/web/src/components/task/merge.js
new file mode 100644
index 0000000000..00aef81986
--- /dev/null
+++ b/ymir/web/src/components/task/merge.js
@@ -0,0 +1,140 @@
+import React, { useCallback, useEffect, useState } from "react"
+import { Form, message } from "antd"
+import { useHistory, useParams } from "umi"
+
+import { formLayout } from "@/config/antd"
+import t from "@/utils/t"
+import useFetch from '@/hooks/useFetch'
+
+import DatasetSelect from "@/components/form/datasetSelect"
+import Desc from "@/components/form/desc"
+import MergeType from "./merge/formItem.mergeType"
+import DatasetName from "@/components/form/items/datasetName"
+import Strategy from "./merge/formItem.strategy"
+import SubmitButtons from "./submitButtons"
+
+import s from "./merge/merge.less"
+
+const { useWatch, useForm } = Form
+
+function Merge({ query = {}, hidden, ok = () => { }, bottom, }) {
+ const [dataset, getDataset, setDataset] = useFetch('dataset/getDataset', {})
+ const [_, clearCache] = useFetch('dataset/clearCache')
+ const [mergeResult, merge] = useFetch('task/merge')
+ const pageParams = useParams()
+ const pid = Number(pageParams.id)
+ const { did, mid, iterationId, currentStage, outputKey, } = query
+ const history = useHistory()
+ const [form] = useForm()
+ const [group, setGroup] = useState()
+ const includes = useWatch('includes', form)
+ const excludes = useWatch('excludes', form)
+ const type = useWatch('type', form)
+ const selectedDataset = useWatch('dataset', form)
+
+
+ const initialValues = {
+ includes: mid ? (Array.isArray(mid) ? mid : mid.split(',').map(Number)) : [],
+ }
+
+ useEffect(() => {
+ did && getDataset({ id: did })
+ }, [did])
+
+ useEffect(() => dataset.id && setGroup(dataset.groupId), [dataset])
+
+ useEffect(() => {
+ if (mergeResult) {
+ ok(mergeResult)
+ message.info(t('task.fusion.create.success.msg'))
+ }
+ }, [mergeResult])
+
+ const checkInputs = (i) => {
+ return i?.excludes?.length || i?.includes?.length
+ }
+
+ const onFinish = async (values) => {
+ if (!checkInputs(values)) {
+ return message.error(t('dataset.merge.validate.inputs'))
+ }
+ const params = {
+ ...values,
+ group: type ? group : undefined,
+ projectId: pid,
+ datasets: [did, selectedDataset, ...(values.includes || [])].filter(item => item),
+ }
+ await merge(params)
+ }
+
+ const onFinishFailed = (err) => {
+ console.log("on finish failed: ", err)
+ }
+
+ function filter(datasets, ids = []) {
+ return datasets.filter(ds => ![...ids, did].includes(ds.id))
+ }
+
+ function originDatasetChange(_, option) {
+ setDataset(option?.dataset || {})
+ setGroup(option?.dataset?.groupId || undefined)
+ }
+
+ const originFilter = useCallback(datasets => filter(datasets, [...(includes || []), ...(excludes || [])]), [includes, excludes])
+
+ const includesFilter = useCallback(datasets => filter(datasets, [selectedDataset, ...(excludes || [])]), [selectedDataset, excludes])
+
+ const excludesFilter = useCallback(datasets => filter(datasets, [selectedDataset, ...(includes || [])]), [selectedDataset, includes])
+
+ return (
+
+ {bottom ? bottom : }
+
+
+ )
+}
+
+export default Merge
diff --git a/ymir/web/src/components/task/merge/formItem.mergeType.js b/ymir/web/src/components/task/merge/formItem.mergeType.js
new file mode 100644
index 0000000000..9107a6a2a4
--- /dev/null
+++ b/ymir/web/src/components/task/merge/formItem.mergeType.js
@@ -0,0 +1,21 @@
+import { Form } from "antd"
+import RadioGroup from "@/components/form/radioGroup"
+import t from '@/utils/t'
+
+const options = [
+ { value: 0, label: 'new' },
+ { value: 1, label: 'exist' },
+]
+const MergeType = ({ initialValue = 0, disabled = [] }) => (
+
+ ({
+ ...option,
+ disabled: disabled.includes(option.value)
+ }))}
+ labelPrefix='task.merge.type.'
+ />
+
+)
+
+export default MergeType
diff --git a/ymir/web/src/components/task/merge/formItem.strategy.js b/ymir/web/src/components/task/merge/formItem.strategy.js
new file mode 100644
index 0000000000..88d40901ff
--- /dev/null
+++ b/ymir/web/src/components/task/merge/formItem.strategy.js
@@ -0,0 +1,23 @@
+import { Form } from "antd"
+import RadioGroup from "@/components/form/radioGroup"
+import t from '@/utils/t'
+
+const options = [
+ { value: 2, label: 'latest' },
+ { value: 3, label: 'original' },
+ { value: 1, label: 'terminate' },
+]
+
+const Strategy = ({ initialValue = 2, hidden = true, ...rest }) => {
+ const prefix = 'task.train.form.repeatdata.'
+ return
+
+
+}
+
+export default Strategy
diff --git a/ymir/web/src/components/task/merge/merge.less b/ymir/web/src/components/task/merge/merge.less
new file mode 100644
index 0000000000..543816fbb5
--- /dev/null
+++ b/ymir/web/src/components/task/merge/merge.less
@@ -0,0 +1,18 @@
+.dataset {
+ margin: 0 0 0 30px;
+}
+.keyword {
+ border: 1px solid #ccc;
+ font-size: 16px;
+ padding: 4px 12px;
+ margin-bottom: 10px;
+}
+.classics {
+ display: flex;
+ width: 80%;
+ text-align: left;
+ margin: auto;
+}
+.submit {
+ // margin: 50px 0 0 25%;
+}
diff --git a/ymir/web/src/components/task/mining.js b/ymir/web/src/components/task/mining.js
new file mode 100644
index 0000000000..2b5553e500
--- /dev/null
+++ b/ymir/web/src/components/task/mining.js
@@ -0,0 +1,308 @@
+import React, { useEffect, useState } from "react"
+import { connect } from "dva"
+import { Card, Radio, Button, Form, ConfigProvider, Space, InputNumber } from "antd"
+import { useHistory, useParams, useLocation } from "umi"
+
+
+import { formLayout } from "@/config/antd"
+import t from "@/utils/t"
+import { HIDDENMODULES } from '@/constants/common'
+import { string2Array } from '@/utils/string'
+import { OPENPAI_MAX_GPU_COUNT } from '@/constants/common'
+import { TYPES } from '@/constants/image'
+import { randomNumber } from "@/utils/number"
+import useFetch from '@/hooks/useFetch'
+
+import ModelSelect from "@/components/form/modelSelect"
+import ImageSelect from "@/components/form/imageSelect"
+import DatasetSelect from "@/components/form/datasetSelect"
+import LiveCodeForm from "@/components/form/items/liveCode"
+import { removeLiveCodeConfig } from "@/components/form/items/liveCodeConfig"
+import DockerConfigForm from "@/components/form/items/dockerConfig"
+import OpenpaiForm from "@/components/form/items/openpai"
+import Desc from "@/components/form/desc"
+
+import styles from "./mining.less"
+import SubmitButtons from "./submitButtons"
+
+function Mining({ query = {}, hidden, ok = () => { }, datasetCache, bottom, ...func }) {
+ const pageParams = useParams()
+ const pid = Number(pageParams.id)
+ const history = useHistory()
+ const location = useLocation()
+ const { mid, image, iterationId, currentStage, outputKey } = query
+ const stage = mid ? (Array.isArray(mid) ? mid : mid.split(',').map(Number)) : undefined
+ const did = Number(query.did)
+ const [dataset, setDataset] = useState({})
+ const [selectedModel, setSelectedModel] = useState({})
+ const [form] = Form.useForm()
+ const [seniorConfig, setSeniorConfig] = useState({})
+ const [topk, setTopk] = useState(true)
+ const [gpu_count, setGPU] = useState(0)
+ const [imageHasInference, setImageHasInference] = useState(false)
+ const [live, setLiveCode] = useState(false)
+ const [openpai, setOpenpai] = useState(false)
+ const [sys, getSysInfo] = useFetch('common/getSysInfo', {})
+ const selectOpenpai = Form.useWatch('openpai', form)
+ const [showConfig, setShowConfig] = useState(false)
+
+ useEffect(() => {
+ getSysInfo()
+ }, [])
+
+ useEffect(() => {
+ setGPU(sys.gpu_count || 0)
+ if (!HIDDENMODULES.OPENPAI) {
+ setOpenpai(!!sys.openpai_enabled)
+ }
+ }, [sys])
+
+ useEffect(() => {
+ setGPU(selectOpenpai ? OPENPAI_MAX_GPU_COUNT : sys.gpu_count || 0)
+ }, [selectOpenpai])
+
+ useEffect(() => {
+ did && func.getDataset(did)
+ }, [did])
+
+ useEffect(() => {
+ const cache = datasetCache[did]
+ if (cache) {
+ setDataset(cache)
+ }
+ }, [datasetCache])
+
+ useEffect(() => {
+ const state = location.state
+
+ if (state?.record) {
+ const { task: { parameters, config }, description, } = state.record
+ const {
+ dataset_id,
+ docker_image,
+ docker_image_id,
+ model_id,
+ model_stage_id,
+ top_k,
+ generate_annotations,
+ } = parameters
+ form.setFieldsValue({
+ datasetId: dataset_id,
+ gpu_count: config.gpu_count,
+ modelStage: [model_id, model_stage_id],
+ image: docker_image_id + ',' + docker_image,
+ topk: top_k,
+ inference: generate_annotations,
+ description,
+ })
+ setShowConfig(true)
+
+ setTimeout(() => setConfig(config), 500)
+
+ history.replace({ state: {} })
+ }
+ }, [location.state])
+
+ function imageChange(_, image = {}) {
+ const { url, configs = [] } = image
+ const configObj = configs.find(conf => conf.type === TYPES.MINING) || {}
+ const hasInference = configs.some(conf => conf.type === TYPES.INFERENCE)
+ setImageHasInference(hasInference)
+ form.setFieldsValue({ inference: hasInference })
+ if (!HIDDENMODULES.LIVECODE) {
+ setLiveCode(image.liveCode || false)
+ }
+ setConfig(removeLiveCodeConfig(configObj.config))
+ }
+
+ function setConfig(config = {}) {
+ setSeniorConfig(config)
+ }
+
+ const onFinish = async (values) => {
+ const config = {
+ ...values.hyperparam?.reduce(
+ (prev, { key, value }) => key && value ? { ...prev, [key]: value } : prev,
+ {}),
+ ...(values.live || {}),
+ }
+
+ config['gpu_count'] = form.getFieldValue('gpu_count') || 0
+
+ const img = (form.getFieldValue('image') || '').split(',')
+ const imageId = Number(img[0])
+ const image = img[1]
+ const params = {
+ ...values,
+ name: 'task_mining_' + randomNumber(),
+ projectId: pid,
+ imageId,
+ image,
+ config,
+ }
+ const result = await func.mine(params)
+ result && ok(result.result_dataset)
+ }
+
+ function onFinishFailed(errorInfo) {
+ console.log("Failed:", errorInfo)
+ }
+
+ function setsChange(id, option) {
+ setDataset(option?.dataset || {})
+ }
+
+ function modelChange(stage, options) {
+ if (stage && !stage[1] && options && options[1]) {
+ form.setFieldsValue({ modelStage: [stage[0], options[1]] })
+ }
+ setSelectedModel(options ? options[0].model : [])
+ }
+
+ const initialValues = {
+ modelStage: stage,
+ image: image ? parseInt(image) : undefined,
+ datasetId: did ? did : undefined,
+ topk: 0,
+ gpu_count: 0,
+ }
+ return (
+
+
+ {bottom ? bottom : }
+
+
+
+ )
+}
+
+const props = (state) => {
+ return {
+ datasetCache: state.dataset.dataset,
+ }
+}
+
+const dis = (dispatch) => {
+ return {
+ getSysInfo() {
+ return dispatch({
+ type: "common/getSysInfo",
+ })
+ },
+ getDatasets(pid, force = true) {
+ return dispatch({
+ type: "dataset/queryAllDatasets",
+ payload: { pid, force },
+ })
+ },
+ getDataset(id, force) {
+ return dispatch({
+ type: "dataset/getDataset",
+ payload: { id, force },
+ })
+ },
+ clearCache() {
+ return dispatch({ type: "dataset/clearCache", })
+ },
+ mine(payload) {
+ return dispatch({
+ type: "task/mine",
+ payload,
+ })
+ },
+ updateIteration(params) {
+ return dispatch({
+ type: 'iteration/updateIteration',
+ payload: params,
+ })
+ },
+ }
+}
+
+export default connect(props, dis)(Mining)
diff --git a/ymir/web/src/components/task/mining.less b/ymir/web/src/components/task/mining.less
new file mode 100644
index 0000000000..a9d1e74528
--- /dev/null
+++ b/ymir/web/src/components/task/mining.less
@@ -0,0 +1,21 @@
+// .wrapper {
+// padding: 100px;
+// }
+.searchLabel {
+ width: 200px;
+}
+.cacheSet {
+ margin: 20px 0 0 0;
+}
+.scoop {
+ display: flex;
+ margin: 50px 0 0 0;
+}
+.scoopForm :global(.ant-form-item-control-input-content) {
+ flex: none;
+}
+.paramContainer {
+ border: 1px solid #f4f4f4;
+ padding: 0 10px;
+ margin-bottom: 10px;
+}
diff --git a/ymir/web/src/components/task/progress.js b/ymir/web/src/components/task/progress.js
index fd637ceed6..b1e43c0f1d 100644
--- a/ymir/web/src/components/task/progress.js
+++ b/ymir/web/src/components/task/progress.js
@@ -4,7 +4,7 @@ import { Button, Col, Descriptions, Progress, Row } from "antd"
import t from "@/utils/t"
import { toFixed } from "@/utils/number"
import Terminate from "./terminate"
-import { states } from "@/constants/dataset"
+import { ResultStates } from "@/constants/common"
import { TASKSTATES } from "@/constants/task"
import StateTag from "@/components/task/stateTag"
import s from "./detail.less"
@@ -25,7 +25,7 @@ function TaskProgress({ state, result = {}, task = {}, fresh = () => { }, progre
}
function terminateVisible() {
- const resultReady = state === states.READY
+ const resultReady = state === ResultStates.READY
const isTerminated = task.is_terminated
const isPending = task.state === TASKSTATES.PENDING
return !isPending && resultReady && !isTerminated
@@ -46,14 +46,14 @@ function TaskProgress({ state, result = {}, task = {}, fresh = () => { }, progre
-
- {task.is_terminated && state === states.READY ? t('task.state.terminating') : <>
+ {task.is_terminated && state === ResultStates.READY ? t('task.state.terminating') : <>
- {state === states.VALID
+ {state === ResultStates.VALID
? t("task.column.duration") + ": " + duration
: null}
>}
-
+
{
+ const history = useHistory()
+ return
+
+ {t(label)}
+
+
+
+ history.goBack()}>
+ {t('task.btn.back')}
+
+
+
+}
+
+export default SubmitButtons
diff --git a/ymir/web/src/components/task/training.js b/ymir/web/src/components/task/training.js
new file mode 100644
index 0000000000..d13d5fa864
--- /dev/null
+++ b/ymir/web/src/components/task/training.js
@@ -0,0 +1,424 @@
+import React, { useCallback, useEffect, useState } from "react"
+import { connect } from "dva"
+import { Select, Radio, Button, Form, Space, InputNumber, Tag, Tooltip } from "antd"
+import { formLayout } from "@/config/antd"
+import { useHistory, useLocation, useParams } from "umi"
+
+import t from "@/utils/t"
+import { HIDDENMODULES } from '@/constants/common'
+import { string2Array, generateName } from '@/utils/string'
+import { OPENPAI_MAX_GPU_COUNT } from '@/constants/common'
+import { TYPES } from '@/constants/image'
+import { randomNumber } from "@/utils/number"
+import useFetch from '@/hooks/useFetch'
+
+import ImageSelect from "@/components/form/imageSelect"
+import ModelSelect from "@/components/form/modelSelect"
+import SampleRates from "@/components/dataset/sampleRates"
+import CheckProjectDirty from "@/components/common/CheckProjectDirty"
+import LiveCodeForm from "@/components/form/items/liveCode"
+import { removeLiveCodeConfig } from "@/components/form/items/liveCodeConfig"
+import DockerConfigForm from "@/components/form/items/dockerConfig"
+import OpenpaiForm from "@/components/form/items/openpai"
+import DatasetSelect from "@/components/form/datasetSelect"
+import Desc from "@/components/form/desc"
+import useDuplicatedCheck from "@/hooks/useDuplicatedCheck"
+import TrainFormat from "./training/trainFormat"
+import SubmitButtons from "./submitButtons"
+
+import styles from "./training/training.less"
+
+const TrainType = [{ value: "detection", label: 'task.train.form.traintypes.detect', checked: true }]
+
+const KeywordsMaxCount = 5
+
+function Train({ query = {}, hidden, ok = () => { }, bottom, allDatasets, datasetCache, ...func }) {
+ const pageParams = useParams()
+ const pid = Number(pageParams.id)
+ const history = useHistory()
+ const location = useLocation()
+ const { mid, image, iterationId, outputKey, currentStage, test, from } = query
+ const stage = mid ? (Array.isArray(mid) ? mid : mid.split(',').map(Number)) : undefined
+ const did = Number(query.did)
+ const [selectedKeywords, setSelectedKeywords] = useState([])
+ const [dataset, setDataset] = useState({})
+ const [trainSet, setTrainSet] = useState(null)
+ const [testSet, setTestSet] = useState(null)
+ const [validationDataset, setValidationDataset] = useState(null)
+ const [trainDataset, setTrainDataset] = useState(null)
+ const [testingSetIds, setTestingSetIds] = useState([])
+ const [form] = Form.useForm()
+ const [seniorConfig, setSeniorConfig] = useState({})
+ const [gpu_count, setGPU] = useState(0)
+ const [projectDirty, setProjectDirty] = useState(false)
+ const [live, setLiveCode] = useState(false)
+ const [openpai, setOpenpai] = useState(false)
+ const checkDuplicated = useDuplicatedCheck(submit)
+ const [sys, getSysInfo] = useFetch('common/getSysInfo', {})
+ const [project, getProject] = useFetch('project/getProject', {})
+ const [updated, updateProject] = useFetch('project/updateProject')
+ const [fromCopy, setFromCopy] = useState(false)
+
+ const selectOpenpai = Form.useWatch('openpai', form)
+ const [showConfig, setShowConfig] = useState(false)
+ const iterationContext = from === 'iteration'
+
+ const renderRadio = (types) => ({ ...type, label: t(type.label) }))} />
+
+ useEffect(() => {
+ getSysInfo()
+ getProject({ id: pid })
+ }, [])
+
+ useEffect(() => {
+ setGPU(sys.gpu_count)
+ if (!HIDDENMODULES.OPENPAI) {
+ setOpenpai(!!sys.openpai_enabled)
+ }
+ }, [sys])
+
+ useEffect(() => {
+ setGPU(selectOpenpai ? OPENPAI_MAX_GPU_COUNT : sys.gpu_count || 0)
+ }, [selectOpenpai])
+
+ useEffect(() => {
+ setTestingSetIds(project?.testingSets || [])
+ iterationContext && setSelectedKeywords(project?.keywords || [])
+ }, [project])
+
+ useEffect(() => {
+ if (did && allDatasets?.length) {
+ const isValid = allDatasets.some(ds => ds.id === did)
+ const visibleValue = isValid ? did : null
+ setTrainSet(visibleValue)
+ form.setFieldsValue({ datasetId: visibleValue })
+ }
+ }, [did, allDatasets])
+
+ useEffect(() => {
+ did && func.getDataset(did)
+ }, [did])
+
+ useEffect(() => {
+ const dst = datasetCache[did]
+ dst && setDataset(dst)
+ }, [datasetCache])
+
+ useEffect(() => {
+ pid && func.getDatasets(pid)
+ }, [pid])
+
+ useEffect(() => {
+ trainDataset &&
+ !iterationContext &&
+ !fromCopy &&
+ setAllKeywords()
+ if (!trainDataset && fromCopy) {
+ setSelectedKeywords([])
+ form.setFieldsValue({ keywords: [] })
+ }
+ }, [trainDataset])
+
+ useEffect(() => {
+ const state = location.state
+
+ if (state?.record) {
+ setFromCopy(true)
+ const { task: { parameters, config }, description, } = state.record
+ const {
+ dataset_id,
+ validation_dataset_id,
+ strategy,
+ docker_image,
+ docker_image_id,
+ model_id,
+ model_stage_id,
+ keywords,
+ } = parameters
+ form.setFieldsValue({
+ datasetId: dataset_id,
+ keywords: keywords,
+ testset: validation_dataset_id,
+ gpu_count: config.gpu_count,
+ modelStage: [model_id, model_stage_id],
+ image: docker_image_id + ',' + docker_image,
+ strategy,
+ description,
+ })
+ setTimeout(() => setConfig(config), 500)
+ setTestSet(validation_dataset_id)
+ setTrainSet(dataset_id)
+ setSelectedKeywords(keywords)
+ setShowConfig(true)
+
+ history.replace({ state: {} })
+ }
+ }, [location.state])
+
+ function setAllKeywords() {
+ const kws = trainDataset?.gt?.keywords
+ setSelectedKeywords(kws)
+ form.setFieldsValue({ keywords: kws })
+ }
+
+ function trainSetChange(value, option) {
+ setTrainSet(value)
+ setTrainDataset(option?.dataset)
+ }
+ function validationSetChange(value, option) {
+ setTestSet(value)
+ setValidationDataset(option?.dataset)
+ }
+
+ function imageChange(_, image = {}) {
+ const { configs } = image
+ const configObj = (configs || []).find(conf => conf.type === TYPES.TRAINING) || {}
+ if (!HIDDENMODULES.LIVECODE) {
+ setLiveCode(image.liveCode || false)
+ }
+ setConfig(removeLiveCodeConfig(configObj.config))
+ }
+
+ function setConfig(config = {}) {
+ setSeniorConfig(config)
+ }
+
+ const onFinish = () => checkDuplicated(trainDataset, validationDataset)
+
+ async function submit(strategy) {
+ const values = form.getFieldsValue()
+ const config = {
+ ...values.hyperparam?.reduce(
+ (prev, { key, value }) => key !== '' && value !== '' ? { ...prev, [key]: value } : prev,
+ {}),
+ ...(values.live || {}),
+ }
+ values.trainFormat && (config['export_format'] = values.trainFormat)
+
+ const gpuCount = form.getFieldValue('gpu_count')
+
+ config['gpu_count'] = gpuCount || 0
+
+ const img = (form.getFieldValue('image') || '').split(',')
+ const imageId = Number(img[0])
+ const image = img[1]
+ const params = {
+ ...values,
+ strategy,
+ name: 'group_' + randomNumber(),
+ projectId: pid,
+ keywords: iterationContext ? project.keywords : values.keywords,
+ image,
+ imageId,
+ config,
+ }
+ const result = await func.train(params)
+ result && ok(result.result_model)
+ }
+
+ function onFinishFailed(errorInfo) {
+ console.log("Failed:", errorInfo)
+ }
+
+ const matchKeywords = dataset => dataset.keywords.some(kw => selectedKeywords.includes(kw))
+ const notTestingSet = id => !testingSetIds.includes(id)
+ const trainsetFilters = useCallback(datasets => datasets.filter(ds => {
+ const notTestSet = ds.id !== testSet
+ return notTestSet && notTestingSet(ds.id)
+ }), [testSet, testingSetIds])
+
+ const validationSetFilters = useCallback(datasets => datasets.filter(ds => {
+ const notTrainSet = ds.id !== trainSet
+ return matchKeywords(ds) && notTrainSet && notTestingSet(ds.id)
+ }), [trainSet, selectedKeywords, testingSetIds])
+
+ const getCheckedValue = (list) => list.find((item) => item.checked)["value"]
+ const initialValues = {
+ name: generateName('train_model'),
+ datasetId: did ? did : undefined,
+ testset: Number(test) ? Number(test) : undefined,
+ image: image ? parseInt(image) : undefined,
+ modelStage: stage,
+ trainType: getCheckedValue(TrainType),
+ gpu_count: 1,
+ }
+ return (
+
+
+ setProjectDirty(dirty)}
+ />
+
+
+ {bottom ? bottom : }
+
+
+
+ )
+}
+
+const props = (state) => {
+ return {
+ allDatasets: state.dataset.allDatasets,
+ datasetCache: state.dataset.dataset,
+ }
+}
+
+const dis = (dispatch) => {
+ return {
+ getDatasets(pid, force = true) {
+ return dispatch({
+ type: "dataset/queryAllDatasets",
+ payload: { pid, force },
+ })
+ },
+ getDataset(id, force) {
+ return dispatch({
+ type: "dataset/getDataset",
+ payload: { id, force },
+ })
+ },
+ clearCache() {
+ return dispatch({ type: "model/clearCache", })
+ },
+ train(payload) {
+ return dispatch({
+ type: "task/train",
+ payload,
+ })
+ },
+ updateIteration(params) {
+ return dispatch({
+ type: 'iteration/updateIteration',
+ payload: params,
+ })
+ },
+ }
+}
+
+export default connect(props, dis)(Train)
diff --git a/ymir/web/src/components/task/training/trainFormat.js b/ymir/web/src/components/task/training/trainFormat.js
new file mode 100644
index 0000000000..c8a089df32
--- /dev/null
+++ b/ymir/web/src/components/task/training/trainFormat.js
@@ -0,0 +1,13 @@
+import { Radio } from "antd"
+
+const annotationFormats = ['ark', 'voc', 'ls_json']
+const assetFormats = ['raw', 'lmdb']
+
+const TrainFormat = ({ value, onChange }) => {
+
+ return
+ {assetFormats.map(as => {annotationFormats.map(an => {as}/{an} )}
)}
+
+}
+
+export default TrainFormat
diff --git a/ymir/web/src/components/task/training/training.less b/ymir/web/src/components/task/training/training.less
new file mode 100644
index 0000000000..2f912b2f19
--- /dev/null
+++ b/ymir/web/src/components/task/training/training.less
@@ -0,0 +1,27 @@
+.options {
+ width: 80%;
+ display: flex;
+ margin: auto;
+ align-items: center;
+}
+.form {
+ width: 100%;
+ text-align: left;
+}
+.container {
+ padding: 100px;
+ margin: auto;
+}
+.selectWrap {
+ display: flex;
+ // width: 100%;
+ margin: 50px 0 0 0;
+ h1 {
+ width: 200px;
+ }
+}
+.paramContainer {
+ border: 1px solid #f4f4f4;
+ padding: 0 10px;
+ margin-bottom: 10px;
+}
diff --git a/ymir/web/src/components/task/typeTag.js b/ymir/web/src/components/task/typeTag.js
index e5d4f26214..7378034972 100644
--- a/ymir/web/src/components/task/typeTag.js
+++ b/ymir/web/src/components/task/typeTag.js
@@ -1,9 +1,8 @@
import { getTaskTypeLabel } from '@/constants/task'
import t from '@/utils/t'
-const TypeTag = ({ type = 0 }) => {
-
- return t(getTaskTypeLabel(type))
+const TypeTag = ({ type }) => {
+ return type ? t(getTaskTypeLabel(type)) : null
}
export default TypeTag
diff --git a/ymir/web/src/config/__test__/antd.test.js b/ymir/web/src/config/__test__/antd.test.js
index 54c553050d..e8c3ec535b 100644
--- a/ymir/web/src/config/__test__/antd.test.js
+++ b/ymir/web/src/config/__test__/antd.test.js
@@ -3,7 +3,7 @@ import { formLayout, tailLayout, layout420 } from "../antd"
describe("config: antd", () => {
it("formLayout", () => {
expect(formLayout.labelCol.span).toBe(6)
- expect(formLayout.wrapperCol.span).toBe(16)
+ expect(formLayout.wrapperCol.span).toBe(12)
})
it("tailLayout", () => {
expect(tailLayout.wrapperCol.offset).toBe(6)
diff --git a/ymir/web/src/config/antd.js b/ymir/web/src/config/antd.js
index ebf5e854dd..3992af19cf 100644
--- a/ymir/web/src/config/antd.js
+++ b/ymir/web/src/config/antd.js
@@ -4,7 +4,10 @@ export const tailLayout = {
export const formLayout = {
labelCol: { span: 6, offset: 2, },
- wrapperCol: { span: 16 },
+ wrapperCol: { span: 12 },
+ labelAlign: 'left',
+ colon: false,
+ scrollToFirstError: true,
}
export const layout420 = {
diff --git a/ymir/web/src/config/routes.ts b/ymir/web/src/config/routes.ts
index c6a39e5128..272d87d79d 100644
--- a/ymir/web/src/config/routes.ts
+++ b/ymir/web/src/config/routes.ts
@@ -23,8 +23,8 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.portal',
},
{
- path: "/home/task/fusion/:id",
- name: "taskFilter",
+ path: "/home/project/:id/fusion",
+ name: "taskFusion",
component: "@/pages/task/fusion/index",
title: 'task.fusion.title',
pid: 25,
@@ -32,7 +32,25 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.task.fusion',
},
{
- path: "/home/task/mining/:id",
+ path: "/home/project/:id/merge",
+ name: "taskMerge",
+ component: "@/pages/task/merge/index",
+ title: 'task.merge.title',
+ pid: 25,
+ id: 36,
+ breadcrumbLabel: 'breadcrumbs.task.merge',
+ },
+ {
+ path: "/home/project/:id/filter",
+ name: "taskFilter",
+ component: "@/pages/task/filter/index",
+ title: 'task.filter.title',
+ pid: 25,
+ id: 37,
+ breadcrumbLabel: 'breadcrumbs.task.filter',
+ },
+ {
+ path: "/home/project/:id/mining",
name: "taskMining",
component: "@/pages/task/mining/index",
title: 'task.mining.title',
@@ -41,7 +59,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.task.mining',
},
{
- path: "/home/task/train/:id",
+ path: "/home/project/:id/train",
name: "taskTrain",
component: "@/pages/task/train/index",
title: 'task.train.title',
@@ -50,7 +68,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.task.training',
},
{
- path: "/home/task/inference/:id",
+ path: "/home/project/:id/inference",
name: "taskInference",
component: "@/pages/task/inference/index",
title: 'task.inference.title',
@@ -59,7 +77,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.task.inference',
},
{
- path: "/home/task/label/:id",
+ path: "/home/project/:id/label",
name: "taskLabel",
component: "@/pages/task/label/index",
title: 'task.label.title',
@@ -68,7 +86,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.task.label',
},
{
- path: "/home/task/copy/:id",
+ path: "/home/project/:id/copy",
name: "taskCopy",
component: "@/pages/task/copy/index",
title: 'dataset.copy.title',
@@ -77,7 +95,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.datasets.copy',
},
{
- path: "/home/dataset/add/:id",
+ path: "/home/project/:id/dataset/add",
name: "datasetImport",
component: "@/pages/dataset/add",
title: "dataset.add.title",
@@ -86,20 +104,20 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.dataset.add',
},
{
- path: "/home/project/:id/dataset/:gid/compare/:ids",
- name: "taskCompare",
- component: "@/pages/task/compare/index",
- title: 'dataset.compare.title',
+ path: "/home/project/:id/dataset/analysis",
+ name: "datasetAnalysis",
+ component: "@/pages/dataset/analysis",
+ title: "dataset.analysis.title",
pid: 25,
- id: 31,
- breadcrumbLabel: 'breadcrumbs.dataset.compare',
+ id: 38,
+ breadcrumbLabel: 'breadcrumbs.dataset.analysis',
},
{
path: "/home/project/:id/dataset/:did",
name: "datasetDetail",
component: "@/pages/dataset/detail",
title: "dataset.title",
- pid: 25,
+ pid: 32,
id: 10,
breadcrumbLabel: 'breadcrumbs.dataset',
},
@@ -113,16 +131,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.dataset.assets',
},
{
- path: "/home/project/:id/model/:mid",
- name: "modelDetail",
- component: "@/pages/model/detail",
- title: "model.title",
- pid: 25,
- id: 12,
- breadcrumbLabel: 'breadcrumbs.model',
- },
- {
- path: "/home/model/import/:id",
+ path: "/home/project/:id/model/import",
name: "modelImport",
component: "@/pages/model/add",
title: "model.title",
@@ -130,12 +139,21 @@ export const homeRoutes = [
id: 13,
breadcrumbLabel: 'breadcrumbs.model.add',
},
+ {
+ path: "/home/project/:id/model/:mid",
+ name: "modelDetail",
+ component: "@/pages/model/detail",
+ title: "model.title",
+ pid: 33,
+ id: 12,
+ breadcrumbLabel: 'breadcrumbs.model',
+ },
{
path: "/home/project/:id/model/:mid/verify",
name: "modelVerify",
component: "@/pages/model/verify",
title: "model.verify.title",
- pid: 25,
+ pid: 33,
id: 15,
breadcrumbLabel: 'breadcrumbs.model.verify',
},
@@ -203,23 +221,59 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.projects',
},
{
- path: "/home/project/detail/:id",
+ path: "/home/project/:id/detail",
name: "projectDetail",
component: "@/pages/project/detail",
- title: "project.title",
+ title: "project.summary",
pid: 24,
id: 25,
- breadcrumbLabel: 'breadcrumbs.project',
+ breadcrumbLabel: 'breadcrumbs.project.summary',
},
{
- path: "/home/project/add/:id",
- name: "projectAdd",
+ path: "/home/project/:id/dataset",
+ name: "projectDataset",
+ component: "@/pages/project/dataset",
+ title: "datasets.title",
+ pid: 25,
+ id: 32,
+ breadcrumbLabel: 'breadcrumbs.datasets',
+ },
+ {
+ path: "/home/project/:id/model",
+ name: "projectModel",
+ component: "@/pages/project/models",
+ title: "models.title",
+ pid: 25,
+ id: 33,
+ breadcrumbLabel: 'breadcrumbs.models',
+ },
+ {
+ path: "/home/project/:id/diagnose",
+ name: "diagnose",
+ component: "@/pages/project/diagnose",
+ title: "model.diagnose.title",
+ pid: 25,
+ id: 34,
+ breadcrumbLabel: 'breadcrumbs.model.diagnose',
+ },
+ {
+ path: "/home/project/:id/add",
+ name: "projectEdit",
component: "@/pages/project/add",
title: "project.settings.title",
pid: 25,
id: 26,
breadcrumbLabel: 'breadcrumbs.project.edit',
},
+ {
+ path: "/home/project/:id/iterations/settings",
+ name: "projectEdit",
+ component: "@/pages/project/iterationSettings",
+ title: "project.iteration.add.title",
+ pid: 25,
+ id: 35,
+ breadcrumbLabel: 'project.iteration.settings.title',
+ },
{
path: "/home/project/add",
name: "projectAdd",
@@ -230,7 +284,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.project.add',
},
{
- path: "/home/project/hidden/:id",
+ path: "/home/project/:id/hidden",
name: "hidden",
component: "@/pages/project/hidden",
title: "project.hidden.title",
@@ -239,7 +293,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.project.hidden',
},
{
- path: "/home/project/iterations/:id",
+ path: "/home/project/:id/iterations",
name: "projectIteration",
component: "@/pages/project/iterations",
title: "project.iterations.title",
@@ -248,7 +302,7 @@ export const homeRoutes = [
breadcrumbLabel: 'breadcrumbs.project.iterations',
},
{
- path: "/home/project/initmodel/:id",
+ path: "/home/project/:id/initmodel",
name: "initModel",
component: "@/pages/iteration/initModel",
title: "project.iteration.initmodel",
@@ -256,6 +310,15 @@ export const homeRoutes = [
id: 28,
breadcrumbLabel: 'breadcrumbs.project.initmodel',
},
+ {
+ path: "/home/algo/:module?",
+ name: "initModel",
+ component: "@/pages/algo/index",
+ title: "algo.title",
+ pid: 0,
+ id: 39,
+ breadcrumbLabel: 'algo.title',
+ },
]
const Routes = [
diff --git a/ymir/web/src/constants/__test__/dataset.test.js b/ymir/web/src/constants/__test__/dataset.test.js
index c83be12028..89236b4be0 100644
--- a/ymir/web/src/constants/__test__/dataset.test.js
+++ b/ymir/web/src/constants/__test__/dataset.test.js
@@ -16,47 +16,52 @@ jest.mock('umi', () => {
const createTime = "2022-03-10T03:39:09"
const task = {
- "name": "t00000020000013277a01646883549",
- "type": 105,
- "project_id": 1,
- "is_deleted": false,
- "create_datetime": createTime,
- "update_datetime": createTime,
- "id": 1,
- "hash": "t00000020000013277a01646883549",
- "state": 3,
- "error_code": null,
- "duration": 18,
- "percent": 1,
- "parameters": {},
- "config": {},
- "user_id": 2,
- "last_message_datetime": "2022-03-10T03:39:09.033206",
- "is_terminated": false,
- "result_type": null
+ name: "t00000020000013277a01646883549",
+ type: 105,
+ project_id: 1,
+ is_deleted: false,
+ create_datetime: createTime,
+ update_datetime: createTime,
+ id: 1,
+ hash: "t00000020000013277a01646883549",
+ state: 3,
+ error_code: null,
+ duration: 18,
+ percent: 1,
+ parameters: {},
+ config: {},
+ user_id: 2,
+ last_message_datetime: "2022-03-10T03:39:09.033206",
+ is_terminated: false,
+ result_type: null
}
const ds = id => ({
- "group_name": "dataset_training",
- "result_state": 1,
- "project_id": 234,
- "dataset_group_id": 1,
+ group_name: "dataset_training",
+ result_state: 1,
+ project_id: 234,
+ dataset_group_id: 1,
state: 1,
- "keywords": { cat: 143, dog: 145 },
- "ignored_keywords": { person: 34 },
- negative_info: {},
- "asset_count": 234,
- "keyword_count": 2,
+ keywords: {
+ gt: { cat: 143, dog: 145 },
+ pred: {}
+ },
+ negative_info: {
+ gt: 34,
+ pred: 234,
+ },
+ asset_count: 234,
+ keyword_count: 2,
'is_protected': false,
- "is_deleted": false,
- "create_datetime": createTime,
- "update_datetime": createTime,
- "id": id,
- "hash": "t00000020000012afef21646883528",
- "version_num": 1,
- "task_id": 1,
- "user_id": 2,
- "related_task": task,
+ is_deleted: false,
+ create_datetime: createTime,
+ update_datetime: createTime,
+ id: id,
+ hash: "t00000020000012afef21646883528",
+ version_num: 1,
+ task_id: 1,
+ user_id: 2,
+ related_task: task,
})
describe("constants: dataset", () => {
@@ -96,11 +101,19 @@ describe("constants: dataset", () => {
assetCount: 234,
keywords: ['cat', 'dog'],
keywordCount: 2,
- keywordsCount: { cat: 143, dog: 145 },
- nagetiveCount: 0,
+ gt: {
+ count: { cat: 143, dog: 145 },
+ keywords: ['cat', 'dog'],
+ negative: 34,
+ total: 234,
+ },
+ pred: {
+ count: {},
+ keywords: [],
+ negative: 234,
+ total: 234,
+ },
isProtected: false,
- projectNagetiveCount: 0,
- ignoredKeywords: ['person'],
hash: 't00000020000012afef21646883528',
state: 1,
hidden: true,
@@ -114,6 +127,7 @@ describe("constants: dataset", () => {
durationLabel: '1 分钟',
taskName: task.name,
task,
+ description: '',
}
expect(transferDataset(dataset)).toEqual(expected)
})
diff --git a/ymir/web/src/constants/__test__/image.test.js b/ymir/web/src/constants/__test__/image.test.js
index 8a57ffa51b..64b03802b7 100644
--- a/ymir/web/src/constants/__test__/image.test.js
+++ b/ymir/web/src/constants/__test__/image.test.js
@@ -1,4 +1,5 @@
-import { TYPES, STATES, imageIsPending, getImageTypeLabel, getImageStateLabel } from '../image'
+import { format } from '@/utils/date'
+import { TYPES, STATES, imageIsPending, getImageTypeLabel, getImageStateLabel, transferImage } from '../image'
describe("constants: image", () => {
it("image type have right mapping and object is freeze", () => {
@@ -7,7 +8,7 @@ describe("constants: image", () => {
expect(TYPES.UNKOWN).toBe(0)
expect(TYPES.INFERENCE).toBe(9)
- function tryExtendAttr () { TYPES.newAttr = 'test' }
+ function tryExtendAttr() { TYPES.newAttr = 'test' }
expect(tryExtendAttr).toThrowError('object is not extensible')
})
it("image states have right mapping and object is freeze", () => {
@@ -15,7 +16,7 @@ describe("constants: image", () => {
expect(STATES.DONE).toBe(3)
expect(STATES.ERROR).toBe(4)
- function tryExtendAttr () { STATES.newAttr = 'test' }
+ function tryExtendAttr() { STATES.newAttr = 'test' }
expect(tryExtendAttr).toThrowError('object is not extensible')
})
it('imageIsPending: image state is pending', () => {
@@ -35,7 +36,7 @@ describe("constants: image", () => {
expect(trainLabel).toEqual(['image.type.train'])
expect(miningLabel).toEqual(['image.type.mining'])
- expect(inferenceLabel).toEqual(['image.type.train','image.type.inference'])
+ expect(inferenceLabel).toEqual(['image.type.train', 'image.type.inference'])
expect(emptyLabel).toEqual([])
expect(unmatchLabel).toEqual([undefined])
@@ -53,4 +54,47 @@ describe("constants: image", () => {
expect(emptyLabel).toBe('')
expect(unmatchLabel).toBe(undefined)
})
+ it('transferImage: transfer image from backend data', () => {
+ const config = (id, type) => ({
+ image_id: id,
+ config: {
+ expected_map: 0.983,
+ idle_seconds: 60,
+ trigger_crash: 0,
+ type: 1
+ },
+ type
+ })
+ const createTime = "2022-03-10T03:39:09"
+ const functions = [1, 2, 9]
+ const configs = functions.map(conf => config(1, conf))
+ const backendData = {
+ name: "sample_image",
+ state: 3,
+ hash: "f3da055bacc7",
+ url: "sample-tmi:stage-test-01",
+ description: "test",
+ is_deleted: false,
+ create_datetime: createTime,
+ id: 1,
+ is_shared: false,
+ related: [],
+ configs,
+ }
+ const image = transferImage(backendData)
+ const expected = {
+ configs,
+ createTime: format(createTime),
+ description: "test",
+ functions,
+ liveCode: undefined,
+ id: 1,
+ isShared: false,
+ name: "sample_image",
+ related: [],
+ state: 3,
+ url: "sample-tmi:stage-test-01",
+ }
+ expect(image).toEqual(expected)
+ })
})
diff --git a/ymir/web/src/constants/__test__/iteration.test.js b/ymir/web/src/constants/__test__/iteration.test.js
new file mode 100644
index 0000000000..6768bfef38
--- /dev/null
+++ b/ymir/web/src/constants/__test__/iteration.test.js
@@ -0,0 +1,83 @@
+import {
+ Stages,
+ getStageLabel,
+ StageList,
+ transferIteration,
+} from '../iteration'
+
+jest.mock('umi', () => {
+ return {
+ getLocale() {
+ return 'zh-CN'
+ },
+ }
+})
+
+const createTime = "2022-03-10T03:39:09"
+
+describe("constants: project", () => {
+ it("function -> getStageLabel.", () => {
+ expect(getStageLabel(Stages.prepareMining, 1)).toBe('project.iteration.stage.ready')
+ expect(getStageLabel(Stages.mining, 1)).toBe('project.iteration.stage.mining')
+ expect(getStageLabel(Stages.labelling, 1)).toBe('project.iteration.stage.label')
+ expect(getStageLabel(Stages.merging, 1)).toBe('project.iteration.stage.merge')
+ expect(getStageLabel(Stages.training, 1)).toBe('project.iteration.stage.training')
+ expect(getStageLabel(Stages.next, 1)).toBe('project.iteration.stage.next')
+ expect(getStageLabel(Stages.next)).toBe('project.iteration.stage.prepare')
+ expect(getStageLabel(Stages.next, 0)).toBe('project.iteration.stage.prepare')
+
+ })
+ it("function -> StageList.", () => {
+ const stageList = StageList()
+
+ expect(stageList.list).toBeInstanceOf(Array)
+ expect(stageList.list.length).toBe(6)
+
+ stageList.list.map(({ value }) => value).forEach(stage => {
+ const stageObj = stageList[stage]
+ expect(stageObj).toBeInstanceOf(Object)
+ expect(stageObj.value).toBeDefined()
+ expect(stageObj.label).toBeDefined()
+ })
+
+ })
+ it("function -> transferIteration.", () => {
+ const origin = {
+ "iteration_round": 1,
+ "previous_iteration": 0,
+ "description": null,
+ "current_stage": 1,
+ "mining_dataset_id": 20,
+ "mining_input_dataset_id": 39,
+ "mining_output_dataset_id": null,
+ "label_output_dataset_id": null,
+ "training_input_dataset_id": null,
+ "training_output_model_id": null,
+ "validation_dataset_id": null,
+ "user_id": 2,
+ "project_id": 8,
+ "is_deleted": false,
+ "create_datetime": "2022-04-13T10:03:49",
+ "update_datetime": "2022-04-13T10:04:02",
+ "id": 3
+ }
+ const expected = {
+ id: 3,
+ projectId: 8,
+ name: undefined,
+ round: 1,
+ currentStage: 1,
+ wholeMiningSet: 20,
+ miningSet: 39,
+ miningResult: null,
+ labelSet: null,
+ trainUpdateSet: null,
+ model: null,
+ trainSet: undefined,
+ testSet: 0,
+ prevIteration: 0
+ }
+ expect(transferIteration(origin)).toEqual(expected)
+
+ })
+})
diff --git a/ymir/web/src/constants/__test__/model.test.js b/ymir/web/src/constants/__test__/model.test.js
index abf7ef4734..56034f6947 100644
--- a/ymir/web/src/constants/__test__/model.test.js
+++ b/ymir/web/src/constants/__test__/model.test.js
@@ -25,6 +25,7 @@ const task = {
"state": 3,
"error_code": null,
"duration": 18,
+ durationLabel: '1 分钟',
"percent": 1,
"parameters": {},
"config": {},
@@ -32,9 +33,7 @@ const task = {
"last_message_datetime": "2022-03-10T03:39:09.033206",
"is_terminated": false,
"result_type": null,
- parameters: {
keywords: ['cat', 'dog'],
- }
}
const ms = id => ({
@@ -45,6 +44,7 @@ const ms = id => ({
state: 1,
url: 'test/url',
map: 0.88,
+ keywords: ['cat', 'dog'],
'is_protected': false,
"is_deleted": false,
"create_datetime": createTime,
@@ -100,6 +100,9 @@ describe("constants: model", () => {
durationLabel: '1 分钟',
taskName: task.name,
task,
+ stages: [],
+ recommendStage: 0,
+ description: '',
}
expect(transferModel(model)).toEqual(expected)
})
diff --git a/ymir/web/src/constants/__test__/project.test.js b/ymir/web/src/constants/__test__/project.test.js
index 58c59dca32..f6729c2963 100644
--- a/ymir/web/src/constants/__test__/project.test.js
+++ b/ymir/web/src/constants/__test__/project.test.js
@@ -1,12 +1,5 @@
import { format } from '@/utils/date'
-import {
- Stages,
- getStageLabel,
- StageList,
- getIterationVersion,
- transferProject,
- transferIteration,
-} from '../project'
+import { transferProject, } from '../project'
jest.mock('umi', () => {
return {
@@ -19,34 +12,6 @@ jest.mock('umi', () => {
const createTime = "2022-03-10T03:39:09"
describe("constants: project", () => {
- it("function -> getStageLabel.", () => {
- expect(getStageLabel(Stages.prepareMining, 1)).toBe('project.iteration.stage.ready')
- expect(getStageLabel(Stages.mining, 1)).toBe('project.iteration.stage.mining')
- expect(getStageLabel(Stages.labelling, 1)).toBe('project.iteration.stage.label')
- expect(getStageLabel(Stages.merging, 1)).toBe('project.iteration.stage.merge')
- expect(getStageLabel(Stages.training, 1)).toBe('project.iteration.stage.training')
- expect(getStageLabel(Stages.next, 1)).toBe('project.iteration.stage.next')
- expect(getStageLabel(Stages.next)).toBe('project.iteration.stage.prepare')
- expect(getStageLabel(Stages.next, 0)).toBe('project.iteration.stage.prepare')
-
- })
- it("function -> StageList.", () => {
- const stageList = StageList()
-
- expect(stageList.list).toBeInstanceOf(Array)
- expect(stageList.list.length).toBe(6)
-
- stageList.list.map(({ value }) => value).forEach(stage => {
- const stageObj = stageList[stage]
- expect(stageObj).toBeInstanceOf(Object)
- expect(stageObj.value).toBeDefined()
- expect(stageObj.label).toBeDefined()
- })
-
- })
- it("function -> getIterationVersion.", () => {
- expect(getIterationVersion(1)).toBe('V1')
- })
it("function -> transferProject.", () => {
const origin = {
"name": "project002",
@@ -63,7 +28,7 @@ describe("constants: project", () => {
"id": 1,
"training_dataset_group_id": 1,
"mining_dataset_id": null,
- "testing_dataset_id": null,
+ "validation_dataset_id": null,
"initial_model_id": null,
"initial_training_dataset_id": 1,
"current_iteration": null,
@@ -77,15 +42,20 @@ describe("constants: project", () => {
"update_datetime": createTime,
"id": 1,
},
- "testing_dataset": null,
+ "validation_dataset": null,
"mining_dataset": null,
+ "testing_datasets": [],
"dataset_count": 6,
"model_count": 0,
"training_keywords": [
"cat",
"person"
],
- "current_iteration_id": null
+ "current_iteration_id": null,
+ "enable_iteration": true,
+ "total_asset_count": 0,
+ "running_task_count": 0,
+ "total_task_count": 0,
}
const expected = {
@@ -102,8 +72,10 @@ describe("constants: project", () => {
trainSetVersion: 1,
testSet: undefined,
miningSet: undefined,
+ testingSets: [],
setCount: 6,
model: 0,
+ modelStage: undefined,
modelCount: 0,
miningStrategy: 0,
chunkSize: 0,
@@ -116,46 +88,14 @@ describe("constants: project", () => {
description: 'project002 desc',
type: 1,
isExample: false,
- updateTime: format(createTime)
+ updateTime: format(createTime),
+ enableIteration: true,
+ totalAssetCount: 0,
+ runningTaskCount: 0,
+ totalTaskCount: 0,
+ candidateTrainSet: 0,
}
expect(transferProject(origin)).toEqual(expected)
})
- it("function -> transferIteration.", () => {
- const origin = {
- "iteration_round": 1,
- "previous_iteration": 0,
- "description": null,
- "current_stage": 1,
- "mining_input_dataset_id": 39,
- "mining_output_dataset_id": null,
- "label_output_dataset_id": null,
- "training_input_dataset_id": null,
- "training_output_model_id": null,
- "testing_dataset_id": null,
- "user_id": 2,
- "project_id": 8,
- "is_deleted": false,
- "create_datetime": "2022-04-13T10:03:49",
- "update_datetime": "2022-04-13T10:04:02",
- "id": 3
- }
- const expected = {
- id: 3,
- projectId: 8,
- name: undefined,
- round: 1,
- currentStage: 1,
- miningSet: 39,
- miningResult: null,
- labelSet: null,
- trainUpdateSet: null,
- model: null,
- trainSet: undefined,
- testSet: 0,
- prevIteration: 0
- }
- expect(transferIteration(origin)).toEqual(expected)
-
- })
})
diff --git a/ymir/web/src/constants/__test__/user.test.js b/ymir/web/src/constants/__test__/user.test.js
index 386f66e5be..ca40e7cc6c 100644
--- a/ymir/web/src/constants/__test__/user.test.js
+++ b/ymir/web/src/constants/__test__/user.test.js
@@ -26,13 +26,6 @@ describe("constants: user", () => {
expect(getRolesLabel(ROLES.SUPER)).toBe('user.role.super')
expect(getRolesLabel(ROLES.ADMIN)).toBe('user.role.admin')
expect(getRolesLabel(ROLES.USER)).toBe('user.role.user')
-
- const allLabels = getRolesLabel()
- expect(allLabels[ROLES.SUPER]).toBe('user.role.super')
- expect(allLabels[ROLES.ADMIN]).toBe('user.role.admin')
- expect(allLabels[ROLES.USER]).toBe('user.role.user')
-
- expect(getRolesLabel('54')).toBe(undefined) // unmatch role
})
it('getUserState: get label of user states', () => {
@@ -40,14 +33,6 @@ describe("constants: user", () => {
expect(getUserState(STATES.ACTIVE)).toBe('user.state.active')
expect(getUserState(STATES.DECLINED)).toBe('user.state.declined')
expect(getUserState(STATES.DEACTIVED)).toBe('user.state.deactived')
-
- const allLabels = getUserState()
- expect(allLabels[STATES.REGISTERED]).toBe('user.state.registered')
- expect(allLabels[STATES.ACTIVE]).toBe('user.state.active')
- expect(allLabels[STATES.DECLINED]).toBe('user.state.declined')
- expect(allLabels[STATES.DEACTIVED]).toBe('user.state.deactived')
-
- expect(getUserState('54')).toBe(undefined) // unmatch state
})
})
diff --git a/ymir/web/src/constants/common.ts b/ymir/web/src/constants/common.ts
index bbecfe04cd..6002587cc9 100644
--- a/ymir/web/src/constants/common.ts
+++ b/ymir/web/src/constants/common.ts
@@ -2,7 +2,20 @@
import { BackendData } from "@/interface/common"
-export enum states {
+export const HIDDENMODULES = {
+ OPENPAI: true,
+ LIVECODE: true,
+}
+
+
+declare global {
+ interface Window {
+ baseConfig: {
+ [name: string]: string,
+ }
+ }
+}
+export enum ResultStates {
READY = 0,
VALID = 1,
INVALID = 2,
@@ -14,15 +27,24 @@ export enum actions {
del = 'delete',
}
+export const OPENPAI_MAX_GPU_COUNT = 8
+
type Result = {
[key: string]: any,
}
export function updateResultState(result: Result, tasks: BackendData) {
const task = tasks[result?.task?.hash]
+ if (!result || !task) {
+ return result
+ }
+ return updateResultByTask(result, task)
+}
+
+export function updateResultByTask(result: Result, task: BackendData) {
if (!result || !task) {
return
}
- if ([states.VALID, states.INVALID].includes(task.result_state)) {
+ if (ResultStates.VALID === task.result_state) {
result.needReload = true
}
result.state = task.result_state
@@ -31,4 +53,32 @@ export function updateResultState(result: Result, tasks: BackendData) {
result.task.state = task.state
result.task.percent = task.percent
return result
-}
\ No newline at end of file
+}
+
+export function validState(state: number) {
+ return ResultStates.VALID === state
+}
+export function invalidState(state: number) {
+ return ResultStates.INVALID === state
+}
+export function readyState(state: number) {
+ return ResultStates.READY === state
+}
+export const statesLabel = (state: ResultStates) => {
+ const maps = {
+ [ResultStates.READY]: 'dataset.state.ready',
+ [ResultStates.VALID]: 'dataset.state.valid',
+ [ResultStates.INVALID]: 'dataset.state.invalid',
+ }
+ return maps[state]
+}
+
+export function getVersionLabel(version: number) {
+ return `V${version}`
+}
+
+export const getDeployUrl = () => {
+ let url = window?.baseConfig?.DEPLOY_MODULE_URL
+ const onlyPort = /^\d+$/.test(url)
+ return onlyPort ? `${location.protocol}//${location.hostname}:${url}` : url
+}
diff --git a/ymir/web/src/constants/dataset.ts b/ymir/web/src/constants/dataset.ts
index 5c5cf88943..dbe124cb8d 100644
--- a/ymir/web/src/constants/dataset.ts
+++ b/ymir/web/src/constants/dataset.ts
@@ -1,8 +1,9 @@
import { getLocale } from "umi"
-import { DatasetGroup, Dataset } from "@/interface/dataset"
+import { DatasetGroup, Dataset, DatasetAnalysis, Annotation, Asset } from "@/interface/dataset"
import { calDuration, format } from '@/utils/date'
-import { getIterationVersion, transferIteration } from "./project"
+import { getVersionLabel } from "./common"
import { BackendData } from "@/interface/common"
+import { Project } from "@/interface/project"
export enum states {
READY = 0,
@@ -10,6 +11,23 @@ export enum states {
INVALID = 2,
}
+export enum evaluationTags {
+ tp = 1,
+ fp = 2,
+ fn = 3,
+ mtp = 11,
+}
+
+export const evaluationLabel = (tag: evaluationTags) => {
+ const maps = {
+ [evaluationTags.tp]: 'tp',
+ [evaluationTags.fp]: 'fp',
+ [evaluationTags.fn]: 'fn',
+ [evaluationTags.mtp]: 'mtp',
+ }
+ return maps[tag]
+}
+
export const statesLabel = (state: states) => {
const maps = {
[states.READY]: 'dataset.state.ready',
@@ -17,9 +35,22 @@ export const statesLabel = (state: states) => {
[states.INVALID]: 'dataset.state.invalid',
}
return maps[state]
-}
+}
-export function transferDatasetGroup (data: BackendData) {
+export enum IMPORTSTRATEGY {
+ ALL_KEYWORDS_IGNORE = 1,
+ UNKOWN_KEYWORDS_IGNORE = 2,
+ UNKOWN_KEYWORDS_STOP = 3,
+ UNKOWN_KEYWORDS_AUTO_ADD = 4,
+}
+
+export enum MERGESTRATEGY {
+ NORMAL = 0,
+ HOST = 1,
+ GUEST = 2,
+}
+
+export function transferDatasetGroup(data: BackendData) {
const group: DatasetGroup = {
id: data.id,
projectId: data.project_id,
@@ -30,23 +61,31 @@ export function transferDatasetGroup (data: BackendData) {
return group
}
-export function transferDataset (data: BackendData): Dataset {
- const { negative_images_cnt = 0, project_negative_images_cnt = 0 } = data.negative_info || {}
+
+const tagsCounts = (gt: BackendData = {}, pred: BackendData = {}) => Object.keys(gt).reduce((prev, tag) => {
+ const gtCount = gt[tag] || {}
+ const predCount = pred[tag] || {}
+ return { ...prev, [tag]: { ...gtCount, ...predCount } }
+}, {})
+const tagsTotal = (gt: BackendData = {}, pred: BackendData = {}) => ({ ...gt, ...pred })
+
+export function transferDataset(data: BackendData): Dataset {
+ const { gt = {}, pred = {} } = data.keywords
+ const assetCount = data.asset_count || 0
+ const keywords = [...new Set([...Object.keys(gt), ...Object.keys(pred)])]
return {
id: data.id,
groupId: data.dataset_group_id,
projectId: data.project_id,
name: data.group_name,
version: data.version_num || 0,
- versionName: getIterationVersion(data.version_num),
- assetCount: data.asset_count || 0,
- keywords: Object.keys(data.keywords || {}),
- keywordCount: data.keyword_count || 0,
- keywordsCount: data.keywords || {},
- nagetiveCount: negative_images_cnt,
+ versionName: getVersionLabel(data.version_num),
+ assetCount,
+ keywords,
+ keywordCount: keywords.length,
+ gt: transferAnnotationsCount(gt, data.negative_info?.gt, assetCount),
+ pred: transferAnnotationsCount(pred, data.negative_info?.pred, assetCount),
isProtected: data.is_protected || false,
- projectNagetiveCount: project_negative_images_cnt,
- ignoredKeywords: Object.keys(data.ignored_keywords || {}),
hash: data.hash,
state: data.result_state,
createTime: format(data.create_datetime),
@@ -60,5 +99,128 @@ export function transferDataset (data: BackendData): Dataset {
taskName: data.related_task.name,
task: data.related_task,
hidden: !data.is_visible,
+ description: data.description || '',
+ inferClass: data?.pred?.eval_class_ids,
+ cks: data.cks_count ? transferCK(data.cks_count, data.cks_count_total) : undefined,
+ tags: data.gt ? transferCK(tagsCounts(data?.gt?.tags_count, data?.pred?.tags_count), tagsTotal(data?.gt?.tags_count_total, data?.pred?.tags_count_total)) : undefined,
+ }
+}
+
+export function validDataset(dataset: Dataset | undefined) {
+ return dataset && dataset.state === states.VALID
+}
+
+export function runningDataset(dataset: Dataset | undefined) {
+ return dataset && dataset.state === states.READY
+}
+
+export function canHide(dataset: Dataset, project: Project | undefined) {
+ const p = project || dataset.project
+ return !runningDataset(dataset) && !p?.hiddenDatasets?.includes(dataset.id)
+}
+
+export function transferDatasetAnalysis(data: BackendData): DatasetAnalysis {
+ const { bytes, area, quality, hw_ratio, } = data.hist
+
+ const assetTotal = data.total_assets_count || 0
+ const gt = generateAnno(data.gt)
+ const pred = generateAnno(data.pred)
+ return {
+ name: data.group_name,
+ version: data.version_num || 0,
+ versionName: getVersionLabel(data.version_num),
+ assetCount: assetTotal,
+ totalAssetMbytes: data.total_assets_mbytes,
+ assetBytes: bytes,
+ assetArea: area,
+ assetQuality: quality,
+ assetHWRatio: hw_ratio,
+ gt,
+ pred,
+ inferClass: data?.pred?.eval_class_ids,
+ cks: transferCK(data.cks_count, data.cks_count_total),
+ tags: transferCK(tagsCounts(data.gt.tags_count, data.pred?.tags_count), tagsTotal(data.gt?.tags_count_total, data.pred?.tags_count_total)),
}
}
+
+export function transferAsset(data: BackendData, keywords: Array): Asset {
+ const colors = generateDatasetColors(keywords || data.keywords)
+ const transferAnnotations = (annotations = [], gt = false) =>
+ annotations.map((an: BackendData) => transferAnnotation(an, gt, colors[an.keyword]))
+
+ const annotations = [
+ ...transferAnnotations(data.gt, true),
+ ...transferAnnotations(data.pred),
+ ]
+ const evaluated = annotations.some(annotation => evaluationTags[annotation.cm])
+ return {
+ id: data.id,
+ hash: data.hash,
+ keywords: data.keywords || [],
+ url: data.url,
+ metadata: data.metadata,
+ size: data.size,
+ annotations,
+ evaluated: evaluated,
+ cks: data.cks || {},
+ }
+}
+
+export function transferAnnotation(data: BackendData, gt: boolean = false, color = ''): Annotation {
+ return {
+ ...data,
+ keyword: data.keyword,
+ box: data.box,
+ cm: data.cm,
+ gt,
+ tags: data.tags || {},
+ color,
+ }
+}
+
+export function transferAnnotationsCount(count = {}, negative = 0, total = 1) {
+ return {
+ keywords: Object.keys(count),
+ count,
+ negative,
+ total,
+ }
+}
+
+const transferCK = (counts: BackendData = {}, total: BackendData = {}) => {
+ const keywords = Object.keys(counts).map(keyword => {
+ const children = counts[keyword]
+ return {
+ keyword,
+ children: Object.keys(children).map(child => ({
+ keyword: child,
+ count: children[child],
+ })),
+ count: total[keyword],
+ }
+ })
+ return {
+ keywords,
+ counts,
+ total,
+ }
+}
+
+const generateAnno = (data: BackendData) => {
+ const { quality, area, area_ratio } = data.hist
+ return {
+ keywords: data.keywords,
+ total: data.annos_count,
+ average: data.ave_annos_count,
+ negative: data.negative_assets_count,
+ quality: quality,
+ area: area,
+ areaRatio: area_ratio,
+ }
+}
+
+function generateDatasetColors(keywords: Array = []): {[name: string]: string} {
+ const KeywordColor = ["green", "red", "cyan", "blue", "yellow", "purple", "magenta", "orange", "gold"]
+ return keywords.reduce((prev, curr, i) =>
+ ({ ...prev, [curr]: KeywordColor[i % KeywordColor.length] }), {})
+}
diff --git a/ymir/web/src/constants/image.ts b/ymir/web/src/constants/image.ts
index ae511b1afc..0b1190a5c8 100644
--- a/ymir/web/src/constants/image.ts
+++ b/ymir/web/src/constants/image.ts
@@ -1,4 +1,6 @@
-import t from '@/utils/t'
+import { BackendData } from "@/interface/common"
+import { Image, InferConfig } from '@/interface/image'
+import { format } from "@/utils/date"
export const TYPES = Object.freeze({
UNKOWN: 0,
@@ -13,7 +15,7 @@ export const STATES = Object.freeze({
ERROR: 4,
})
-export function imageIsPending (state: number) {
+export function imageIsPending(state: number) {
return state === STATES.PENDING
}
@@ -44,3 +46,19 @@ export const getImageStateLabel = (state: number | undefined) => {
}
return labels[state]
}
+
+export function transferImage(data: BackendData): Image {
+ return {
+ id: data.id,
+ name: data.name,
+ state: data.state,
+ functions: (data.configs || []).map((config: InferConfig) => config.type),
+ configs: data.configs || [],
+ url: data.url,
+ liveCode: data.enable_livecode,
+ isShared: data.is_shared,
+ related: data.related,
+ description: data.description,
+ createTime: format(data.create_datetime),
+ }
+}
diff --git a/ymir/web/src/constants/iteration.ts b/ymir/web/src/constants/iteration.ts
new file mode 100644
index 0000000000..07cffc4b58
--- /dev/null
+++ b/ymir/web/src/constants/iteration.ts
@@ -0,0 +1,102 @@
+import { Iteration, } from "@/interface/iteration"
+import { BackendData } from "@/interface/common"
+
+export enum Stages {
+ prepareMining = 0,
+ mining = 1,
+ labelling = 2,
+ merging = 3,
+ training = 4,
+ next = 5,
+}
+export enum MiningStrategy {
+ block = 0,
+ unique = 1,
+ free = 2,
+}
+
+export function getStageLabel(stage: Stages, round: number = 0) {
+ const labels = StageList().list.map(item => item.label)
+ return `project.iteration.stage.${round ? labels[stage] : 'prepare'}`
+}
+
+type stageObject = {
+ value: Stages,
+ result?: string,
+ url?: string,
+}
+
+export const StageList = () => {
+ const iterationParams = 'iterationId={id}¤tStage={stage}&outputKey={output}&from=iteration'
+ const list = [
+ { label: 'ready', value: Stages.prepareMining, output: 'miningSet', input: '', url: `/home/project/{pid}/fusion?did={s0d}&strategy={s0s}&chunk={s0c}&${iterationParams}` },
+ { label: 'mining', value: Stages.mining, output: 'miningResult', input: 'miningSet', url: `/home/project/{pid}/mining?did={s1d}&mid={s1m}&${iterationParams}` },
+ { label: 'label', value: Stages.labelling, output: 'labelSet', input: 'miningResult', url: `/home/project/{pid}/label?did={s2d}&${iterationParams}` },
+ { label: 'merge', value: Stages.merging, output: 'trainUpdateSet', input: 'labelSet', url: `/home/project/{pid}/merge?did={s3d}&mid={s3m}&${iterationParams}` },
+ { label: 'training', value: Stages.training, output: 'model', input: 'trainUpdateSet', url: `/home/project/{pid}/train?did={s4d}&test={s4t}&${iterationParams}` },
+ { label: 'next', value: Stages.next, output: '', input: 'trainSet', },
+ ]
+ return { list, ...singleList(list) }
+}
+
+export function transferIteration(data: BackendData): Iteration | undefined {
+ if (!data) {
+ return
+ }
+ return {
+ id: data.id,
+ projectId: data.project_id,
+ name: data.name,
+ round: data.iteration_round || 0,
+ currentStage: data.current_stage || 0,
+ testSet: data.validation_dataset_id || 0,
+ wholeMiningSet: data.mining_dataset_id || 0,
+ miningSet: data.mining_input_dataset_id,
+ miningResult: data.mining_output_dataset_id,
+ labelSet: data.label_output_dataset_id,
+ trainUpdateSet: data.training_input_dataset_id,
+ model: data.training_output_model_id,
+ trainSet: data.previous_training_dataset_id,
+ prevIteration: data.previous_iteration || 0,
+ }
+}
+
+function singleList(arr: Array) {
+ return arr.reduce((prev, item, index) => ({
+ ...prev,
+ [item.value]: {
+ ...item,
+ next: arr[index + 1] ? arr[index + 1] : null,
+ }
+ }), {})
+}
+
+type Ratio = {
+ class_name: string,
+ processed_assets_count: number,
+ total_assets_count: number,
+}
+
+export function transferMiningStats(data: BackendData) {
+ const { total_mining_ratio, class_wise_mining_ratio, negative_ratio } = data
+ const transfer = (ratios: Array) => {
+ const getName = (ratio: Ratio) => ratio.class_name || ''
+ const keywords = ratios.map(getName)
+ const count = ratios.reduce((prev, item) => {
+ const name = getName(item)
+ return {
+ ...prev,
+ [name]: item.processed_assets_count,
+ [name + '_total']: item.total_assets_count,
+ }
+ }, {})
+ return {
+ keywords,
+ count,
+ }
+ }
+ return {
+ totalList: transfer([total_mining_ratio]),
+ keywordList: transfer(class_wise_mining_ratio),
+ }
+}
diff --git a/ymir/web/src/constants/model.ts b/ymir/web/src/constants/model.ts
index 422c7030ac..b7f2d24435 100644
--- a/ymir/web/src/constants/model.ts
+++ b/ymir/web/src/constants/model.ts
@@ -1,6 +1,6 @@
-import { ModelGroup, ModelVersion } from "@/interface/model"
+import { ModelGroup, ModelVersion, Stage } from "@/interface/model"
import { calDuration, format } from '@/utils/date'
-import { getIterationVersion } from "./project"
+import { getVersionLabel } from "./common"
import { BackendData } from "@/interface/common"
import { getLocale } from "umi"
@@ -10,7 +10,7 @@ export enum states {
INVALID = 2,
}
-export function transferModelGroup (data: BackendData) {
+export function transferModelGroup(data: BackendData) {
const group: ModelGroup = {
id: data.id,
name: data.name,
@@ -20,7 +20,8 @@ export function transferModelGroup (data: BackendData) {
return group
}
-export function transferModel (data: BackendData): ModelVersion {
+export function transferModel(data: BackendData): ModelVersion {
+ const durationLabel = calDuration(data.related_task.duration, getLocale())
return {
id: data.id,
name: data.group_name,
@@ -28,9 +29,9 @@ export function transferModel (data: BackendData): ModelVersion {
projectId: data.project_id,
hash: data.hash,
version: data.version_num || 0,
- versionName: getIterationVersion(data.version_num),
+ versionName: getVersionLabel(data.version_num),
state: data.result_state,
- keywords: data?.related_task?.parameters?.keywords || [],
+ keywords: data?.keywords || [],
map: data.map || 0,
url: data.url || '',
createTime: format(data.create_datetime),
@@ -42,7 +43,65 @@ export function transferModel (data: BackendData): ModelVersion {
taskName: data.related_task.name,
duration: data.related_task.duration,
durationLabel: calDuration(data.related_task.duration, getLocale()),
- task: data.related_task,
+ task: { ...data.related_task, durationLabel, },
hidden: !data.is_visible,
+ stages: data.related_stages || [],
+ recommendStage: data.recommended_stage || 0,
+ description: data.description || '',
}
}
+
+/**
+ * is valid model
+ * @param {ModelVersion} model
+ * @returns {Boolean}
+ */
+export function validModel(model: ModelVersion): Boolean {
+ return model.state === states.VALID
+}
+
+/**
+ * is invalid model
+ * @param {ModelVersion} model
+ * @returns {Boolean}
+ */
+export function invalidModel(model: ModelVersion): Boolean {
+ return model.state === states.INVALID
+}
+
+/**
+ * is running model
+ * @param {ModelVersion} model
+ * @returns {Boolean}
+ */
+export function runningModel(model: ModelVersion): Boolean {
+ return model.state === states.READY
+}
+
+export function getModelName(data: BackendData) {
+ return `${data.model?.group_name} ${getVersionLabel(data.model?.version_num)}`
+}
+
+/**
+ * transfer backend data into stage object
+ * @param {BackendData} data
+ * @returns {Stage}
+ */
+export function transferStage(data: BackendData): Stage {
+ return {
+ id: data.id,
+ name: data.name,
+ map: data.map,
+ modelId: data.model?.id,
+ modelName: getModelName(data),
+ }
+}
+
+/**
+ * get recommend stage from model
+ * @param {ModelVersion} model
+ * @returns {Stage|undefined}
+ */
+export function getRecommendStage(model: ModelVersion): Stage| undefined {
+ return model.stages?.find(stage => stage.id === model.recommendStage)
+}
diff --git a/ymir/web/src/constants/project.ts b/ymir/web/src/constants/project.ts
index eb7957141d..6a4774e0a2 100644
--- a/ymir/web/src/constants/project.ts
+++ b/ymir/web/src/constants/project.ts
@@ -1,54 +1,14 @@
-import { Project, Iteration, } from "@/interface/project"
+import { Project, } from "@/interface/project"
import { BackendData } from "@/interface/common"
import { transferDatasetGroup, transferDataset } from '@/constants/dataset'
import { format } from '@/utils/date'
-
-export enum Stages {
- prepareMining = 0,
- mining = 1,
- labelling = 2,
- merging = 3,
- training = 4,
- next = 5,
-}
-export enum MiningStrategy {
- block = 0,
- unique = 1,
- free = 2,
-}
+import { transferIteration } from "./iteration"
export const tabs = [
{ tab: 'project.tab.set.title', key: 'dataset', },
{ tab: 'project.tab.model.title', key: 'model', },
]
-export function getStageLabel(stage: Stages, round: number = 0) {
- const labels = StageList().list.map(item => item.label)
- return `project.iteration.stage.${round ? labels[stage] : 'prepare'}`
-}
-
-type stageObject = {
- value: Stages,
- result?: string,
- url?: string,
-}
-export const StageList = () => {
- const iterationParams = 'iterationId={id}¤tStage={stage}&outputKey={output}'
- const list = [
- { label: 'ready', value: Stages.prepareMining, output: 'miningSet', input: '', url: `/home/task/fusion/{pid}?did={s0d}&strategy={s0s}&chunk={s0c}&${iterationParams}` },
- { label: 'mining', value: Stages.mining, output: 'miningResult', input: 'miningSet', url: `/home/task/mining/{pid}?did={s1d}&mid={s1m}&${iterationParams}` },
- { label: 'label', value: Stages.labelling, output: 'labelSet', input: 'miningResult', url: `/home/task/label/{pid}?did={s2d}&${iterationParams}` },
- { label: 'merge', value: Stages.merging, output: 'trainUpdateSet', input: 'labelSet', url: `/home/task/fusion/{pid}?did={s3d}&merging={s3m}&${iterationParams}` },
- { label: 'training', value: Stages.training, output: 'model', input: 'trainUpdateSet', url: `/home/task/train/{pid}?did={s4d}&test={s4t}&${iterationParams}` },
- { label: 'next', value: Stages.next, output: '', input: 'trainSet', },
- ]
- return { list, ...singleList(list) }
-}
-
-export function getIterationVersion(version: number) {
- return `V${version}`
-}
-
export function transferProject(data: BackendData) {
const iteration = transferIteration(data.current_iteration)
const project: Project = {
@@ -56,11 +16,14 @@ export function transferProject(data: BackendData) {
name: data.name,
keywords: data.training_keywords,
trainSet: data.training_dataset_group ? transferDatasetGroup(data.training_dataset_group) : undefined,
- testSet: data.testing_dataset ? transferDataset(data.testing_dataset) : undefined,
+ testSet: data.validation_dataset ? transferDataset(data.validation_dataset) : undefined,
miningSet: data.mining_dataset ? transferDataset(data.mining_dataset) : undefined,
+ testingSets: data.testing_dataset_ids ? data.testing_dataset_ids.split(',').map(Number) : [],
setCount: data.dataset_count,
+ candidateTrainSet: data.candidate_training_dataset_id || 0,
trainSetVersion: data.initial_training_dataset_id || 0,
model: data.initial_model_id || 0,
+ modelStage: data.initial_model_id ? [data.initial_model_id, data.initial_model_stage_id] : undefined,
modelCount: data.model_count,
miningStrategy: data.mining_strategy,
chunkSize: data.chunk_size,
@@ -74,37 +37,10 @@ export function transferProject(data: BackendData) {
hiddenDatasets: data.referenced_dataset_ids || [],
hiddenModels: data.referenced_model_ids || [],
updateTime: format(data.update_datetime),
+ enableIteration: data.enable_iteration,
+ totalAssetCount: data.total_asset_count,
+ runningTaskCount: data.running_task_count,
+ totalTaskCount: data.total_task_count,
}
return project
}
-
-export function transferIteration(data: BackendData): Iteration | undefined {
- if (!data) {
- return
- }
- return {
- id: data.id,
- projectId: data.project_id,
- name: data.name,
- round: data.iteration_round || 0,
- currentStage: data.current_stage || 0,
- testSet: data.testing_dataset_id || 0,
- miningSet: data.mining_input_dataset_id,
- miningResult: data.mining_output_dataset_id,
- labelSet: data.label_output_dataset_id,
- trainUpdateSet: data.training_input_dataset_id,
- model: data.training_output_model_id,
- trainSet: data.previous_training_dataset_id,
- prevIteration: data.previous_iteration || 0,
- }
-}
-
-function singleList(arr: Array) {
- return arr.reduce((prev, item, index) => ({
- ...prev,
- [item.value]: {
- ...item,
- next: arr[index + 1] ? arr[index + 1] : null,
- }
- }), {})
-}
diff --git a/ymir/web/src/constants/task.ts b/ymir/web/src/constants/task.ts
index 8194ed0766..cd49e9b8ba 100644
--- a/ymir/web/src/constants/task.ts
+++ b/ymir/web/src/constants/task.ts
@@ -1,11 +1,11 @@
-import { states } from './dataset'
-
export enum TASKTYPES {
TRAINING = 1,
MINING = 2,
LABEL = 3,
+ FILTER = 4,
IMPORT = 5,
COPY = 7,
+ MERGE = 8,
INFERENCE = 15,
FUSION = 11,
MODELIMPORT = 13,
@@ -31,6 +31,8 @@ export const getTaskTypeLabel = (type: TASKTYPES) => {
[TASKTYPES.MINING]: 'task.type.mining',
[TASKTYPES.LABEL]: 'task.type.label',
[TASKTYPES.FUSION]: 'task.type.fusion',
+ [TASKTYPES.FILTER]: 'task.type.filter',
+ [TASKTYPES.MERGE]: 'task.type.merge',
[TASKTYPES.COPY]: 'task.type.copy',
[TASKTYPES.INFERENCE]: 'task.type.inference',
[TASKTYPES.IMPORT]: 'task.type.import',
diff --git a/ymir/web/src/constants/user.ts b/ymir/web/src/constants/user.ts
index b5030f3553..9373a9b0b7 100644
--- a/ymir/web/src/constants/user.ts
+++ b/ymir/web/src/constants/user.ts
@@ -1,4 +1,3 @@
-import t from "@/utils/t"
export const ROLES = Object.freeze({
SUPER: 3,
@@ -15,20 +14,24 @@ export const STATES = Object.freeze({
export const getRolesLabel = (role: number | undefined) => {
const labels = Object.freeze({
- [ROLES.SUPER]: 'user.role.super',
- [ROLES.ADMIN]: 'user.role.admin',
- [ROLES.USER]: 'user.role.user',
+ [ROLES.SUPER]: 'super',
+ [ROLES.ADMIN]: 'admin',
+ [ROLES.USER]: 'user',
})
- return typeof role !== 'undefined' ? labels[role] : labels
+ return typeof role !== 'undefined' ? `user.role.${labels[role]}` : labels
}
export const getUserState = (state: number | undefined) => {
const states = Object.freeze({
- [STATES.REGISTERED]: 'user.state.registered',
- [STATES.ACTIVE]: 'user.state.active',
- [STATES.DECLINED]: 'user.state.declined',
- [STATES.DEACTIVED]: 'user.state.deactived',
+ [STATES.REGISTERED]: 'registered',
+ [STATES.ACTIVE]: 'active',
+ [STATES.DECLINED]: 'declined',
+ [STATES.DEACTIVED]: 'deactived',
})
- return typeof state !== 'undefined' ? states[state] : states
+ return typeof state !== 'undefined' ? `user.state.${states[state]}` : states
+}
+
+export function isSuperAdmin(role: number) {
+ return ROLES.SUPER === role
}
diff --git a/ymir/web/src/global.less b/ymir/web/src/global.less
index bb91d88c0e..f6ff373113 100644
--- a/ymir/web/src/global.less
+++ b/ymir/web/src/global.less
@@ -1,4 +1,4 @@
-//颜色
+//color
@white: #FFFFFF;
@black: #000000;
@color85: rgba(0, 0, 0, 0.85);
@@ -9,14 +9,14 @@
@font-bule: #2CBDE9;
@font-red: #F2637B;
-//字体
+// font size
@font-size12: 12px;
@font-size14: 14px;
@font-size16: 16px;
@font-size18: 18px;
@font-size20: 20px;
-//间距
+//gap
@gap5: 5px;
@gap8: 8px;
@gap10: 10px;
@@ -24,29 +24,53 @@
@gap20: 20px;
@gap24: 24px;
-//投影变量
+//shadow class
@shadow: 0px 0px 3px;
@shadow-color: rgba(0, 0, 0, 5%);
.box-shadow(@style: @shadow, @color: @shadow-color) {
box-shadow: @style @color;
}
-//圆角变量
+//radius class
@radius: 5px;
.box-radius(@style: @radius) {
border-radius: @style;
}
-//高度变量
+//hight class
@minus: 180px;
.calc-height(@minus: @minus) {
min-height: calc(100vh - @minus);
}
+.error {
+ color: @error-color;
+}
html, body, #root {
height: 100%;
}
+ol, ul {
+ margin: 0;
+ padding: 0;
+}
+
+// public class
+.scrollbar {
+ overflow-y: auto;
+ &::-webkit-scrollbar {
+ width: 1px;
+ background-color: #f4f4f4;
+ }
+ &::-webkit-scrollbar-thumb {
+ width: 10px;
+ background-color: #ddd;
+ border-radius: 10px;
+ }
+}
+.orange {
+ color: orange;
+}
body {
margin: 0;
@@ -65,6 +89,9 @@ body {
vertical-align: middle;
}
}
+ .ant-btn-lg {
+ min-width: 80px;
+ }
.ant-table-thead,.ant-table-tbody {
tr > th, tr >td {
border: none;
@@ -137,8 +164,15 @@ body {
color: @color85;
}
+ /** sidebar **/
+ .sidebar {
+ background: #fff;
+ overflow: auto;
+ height: calc(100vh - 60px);
+ }
+
/** form **/
- .ant-form-item-label {
+ .ant-form-horizontal .ant-form-item-label {
font-weight: bold;
color: @color85;
> label:not(.ant-form-item-required):before {
@@ -154,6 +188,9 @@ body {
.ant-radio-button-wrapper,.ant-form-item-control-input-content {
color:rgba(0, 0, 0, 0.45);
}
+ .ant-form-item-label > label .ant-form-item-tooltip {
+ color: rgba(0, 0, 0, 0.25);
+ }
//radio color styles
.ant-radio-checked .ant-radio-inner {
@@ -184,10 +221,10 @@ body {
.breadcrumb {
height: 50px;
line-height: 50px;
- background: @white;
- margin: 0 -5vw 20px;
- padding: 0 5vw;
- .box-shadow();
+ // background: @white;
+ margin: 0 -20px 0;
+ padding: 0 20px;
+ // .box-shadow();
.breadcrumbContent {
line-height: 50px;
}
@@ -239,6 +276,14 @@ body {
font-size: @font-size18;
}
.title {
+ .nameExtra {
+ background-color: @primary-color;
+ color: @white;
+ border-radius: 20px;
+ font-size: 12px;
+ display: inline-block;
+ padding: 2px 10px;
+ }
.titleItem {
font-size: @font-size14;
font-weight: normal;
@@ -367,6 +412,9 @@ body {
background-color: #fafafa;
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
+ &.nobg {
+ background: none;
+ }
&:before {
content: ' ';
display: inline-block;
@@ -411,4 +459,40 @@ body {
.anticon + span {
margin-left: 5px;
}
-}
\ No newline at end of file
+}
+
+// right side form
+.rightForm {
+ position: relative;
+ .mask {
+ position: absolute;
+ width: calc(100% + 20px);
+ height: calc(100% + 10px);
+ display: flex;
+ align-items: end;
+ justify-content: center;
+ background-color: rgba(0, 0, 0, 0.1);
+ z-index: 5;
+ margin: 0 -10px -10px;
+ }
+}
+
+.fullTab {
+ .ant-card-body {
+ padding: 20px;
+ }
+ .ant-card-head {
+ padding: 0;
+ }
+ .ant-tabs-nav-list {
+ flex: 1;
+ }
+ .ant-tabs-large > .ant-tabs-nav .ant-tabs-tab {
+ padding: 16px 20px;
+ flex: 1;
+ justify-content: center;
+ }
+ .ant-tabs > .ant-tabs-nav .ant-tabs-nav-operations {
+ display: none;
+ }
+}
diff --git a/ymir/web/src/hooks/useCardTitle.tsx b/ymir/web/src/hooks/useCardTitle.tsx
new file mode 100644
index 0000000000..dad695b76c
--- /dev/null
+++ b/ymir/web/src/hooks/useCardTitle.tsx
@@ -0,0 +1,13 @@
+import { Button, Col, Row } from "antd"
+import t from '@/utils/t'
+import { useHistory } from "umi"
+
+export default function useCardTitle(label = '') {
+ const history = useHistory()
+ return (
+
+ {t(label)}
+ history.goBack()}>{t('common.back')}>
+
+ )
+}
\ No newline at end of file
diff --git a/ymir/web/src/hooks/useDuplicatedCheck.js b/ymir/web/src/hooks/useDuplicatedCheck.js
new file mode 100644
index 0000000000..07a905ba82
--- /dev/null
+++ b/ymir/web/src/hooks/useDuplicatedCheck.js
@@ -0,0 +1,83 @@
+import { useCallback, useEffect, useState } from 'react'
+import { Modal, Radio } from 'antd'
+
+import t from '@/utils/t'
+import { MERGESTRATEGY } from '@/constants/dataset'
+import useFetch from '@/hooks/useFetch'
+
+const { confirm, error } = Modal
+
+const options = [
+ { value: MERGESTRATEGY.HOST, label: 'task.train.duplicated.option.train' },
+ { value: MERGESTRATEGY.GUEST, label: 'task.train.duplicated.option.validation' }
+]
+
+const ContentRender = ({ duplicated, strategy, disabled, onChange = () => { } }) => {
+ const [s, setS] = useState(strategy)
+ useEffect(() => onChange(s), [s])
+ return
+
{t('task.train.duplicated.tip', { duplicated })}
+
{ setS(value) }}
+ options={options.map(opt => ({
+ ...opt,
+ disabled: disabled === opt.value,
+ label: {t(opt.label)}
+ }))}
+ />
+
+}
+
+const useDuplicatedCheck = (onChange = () => { }) => {
+ const [_, checkDuplication] = useFetch('dataset/checkDuplication', 0)
+ let strategy = MERGESTRATEGY.NORMAL
+
+ const ok = () => {
+ onChange(strategy)
+ }
+
+ const check = async (trainDataset, validationDataset) => {
+ const result = await checkDuplication({ trainSet: trainDataset?.id, validationSet: validationDataset?.id })
+ if (typeof result !== 'undefined') {
+ checkHandle(result, trainDataset, validationDataset)
+ }
+ }
+
+ const checkHandle = (duplicated, trainDataset, validationDataset) => {
+ if (!duplicated) {
+ return ok()
+ }
+ const allValidation = duplicated === trainDataset.assetCount
+ const allTrain = duplicated === validationDataset.assetCount
+ const allDuplicated = allValidation && allTrain
+ if (allDuplicated) {
+ return error({
+ content: t('task.train.action.duplicated.all'),
+ })
+ }
+ PopConfirm(duplicated, allValidation, allTrain)
+ }
+
+ const PopConfirm = (duplicated, allValidation, allTrain) => {
+
+ const disabled = allValidation ? MERGESTRATEGY.GUEST : (allTrain ? MERGESTRATEGY.HOST : null)
+ const value = allTrain ? MERGESTRATEGY.GUEST : MERGESTRATEGY.HOST
+ strategy = value
+ confirm({
+ visible: true,
+ content: (strategy = value)}
+ />,
+ onOk: ok,
+ destroyOnClose: true,
+ })
+ }
+
+ return check
+}
+
+export default useDuplicatedCheck
diff --git a/ymir/web/src/hooks/useDynamicRender.js b/ymir/web/src/hooks/useDynamicRender.js
index 47d69a8270..360fa8b823 100644
--- a/ymir/web/src/hooks/useDynamicRender.js
+++ b/ymir/web/src/hooks/useDynamicRender.js
@@ -4,9 +4,9 @@ const useDynamicRender = (field = 'ap') => {
const [selected, setSelected] = useState(null)
const render = useCallback((metrics = {}) => {
- const everage = metrics.ci_averaged_evaluation || {}
+ const average = metrics.ci_averaged_evaluation || {}
const kwMetrics = metrics.ci_evaluations || {}
- const result = (selected === '' ? everage : kwMetrics[selected]) || {}
+ const result = (selected === '' ? average : kwMetrics[selected]) || {}
const metric = result[field]
return typeof metric === 'undefined' || metric === -1 ? '-' : metric
}, [selected, field])
diff --git a/ymir/web/src/hooks/useFetch.ts b/ymir/web/src/hooks/useFetch.ts
new file mode 100644
index 0000000000..fffa9f2f0f
--- /dev/null
+++ b/ymir/web/src/hooks/useFetch.ts
@@ -0,0 +1,30 @@
+import { useState } from 'react'
+import { useDispatch } from 'umi'
+
+const useFetch = (effect: string, initResult: any = null, hideLoading: Boolean = false) => {
+ const dispatch = useDispatch()
+ const setLoading = (loading: Boolean) => dispatch({
+ type: 'common/setLoading',
+ payload: loading
+ })
+
+ const fetch = (payload: any) => dispatch({
+ type: effect,
+ payload,
+ })
+ const [result, setResult] = useState(initResult)
+
+ const getResult = async (payload: any) => {
+ setLoading(!hideLoading)
+ const result = await fetch(payload)
+ setLoading(true)
+ if (typeof result !== 'undefined') {
+ setResult(result)
+ }
+ return result
+ }
+
+ return [result, getResult, setResult]
+}
+
+export default useFetch
diff --git a/ymir/web/src/hooks/usePostMessage.ts b/ymir/web/src/hooks/usePostMessage.ts
new file mode 100644
index 0000000000..4dab9c209f
--- /dev/null
+++ b/ymir/web/src/hooks/usePostMessage.ts
@@ -0,0 +1,63 @@
+import { useEffect, useState } from "react"
+import { useHistory } from 'umi'
+
+type Data = {
+ type: string,
+ data: any,
+ [key: string]: any,
+}
+const usePostMessage = (domain: string = '*', fixWin: Window | null = null): Array => {
+ const [recieved, setRecieved] = useState(null)
+ const history = useHistory()
+
+ useEffect(() => {
+ //// send loaded if have parent window
+ window.parent !== window && post('loaded', {}, window.parent)
+ }, [window.parent])
+
+ useEffect(() => {
+ const handle = (ev: MessageEvent) => {
+ const { data, origin } = ev
+ try {
+ if (origin === domain) {
+ const recieveData: Data = JSON.parse(data)
+
+ if (recieveData.type === 'redirect' && recieveData?.data?.path) {
+ history.push(recieveData.data.path)
+ } else {
+ recievedHandle(recieveData)
+ }
+ }
+ } catch (e) {
+ console.error('post message parse error')
+ }
+ }
+ window.addEventListener('message', handle)
+ return () => window.removeEventListener('message', handle)
+ }, [])
+
+ function post(type: string, data = {}, win: Window | null = null) {
+ const target = win || fixWin
+ if (!target) {
+ return console.error('target window is required')
+ }
+ const message = JSON.stringify({
+ type,
+ data,
+ })
+ target.postMessage(message, domain)
+ }
+
+ function recievedHandle(recieveData: Data) {
+ setRecieved({
+ ...recieveData,
+ finish: (data: Object = {}) => {
+ post(`${recieveData.type}_finish`, data)
+ }
+ })
+ }
+
+ return [post, recieved]
+}
+
+export default usePostMessage
diff --git a/ymir/web/src/hooks/useProjectStatus.js b/ymir/web/src/hooks/useProjectStatus.js
deleted file mode 100644
index e168756fdc..0000000000
--- a/ymir/web/src/hooks/useProjectStatus.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useState } from 'react'
-import { useDispatch } from 'umi'
-
-const useProjectStatus = (initPid) => {
- const [pid, setPid] = useState(initPid)
- const dispatch = useDispatch()
- const checkStatus = (pid) => dispatch({ type: 'project/checkStatus', payload: pid, })
-
- const checkDirty = async () => {
- const result = await checkStatus(pid)
- if (result) {
- return result.is_dirty
- }
- }
-
- return {
- setPid,
- checkDirty,
- }
-}
-
-export default useProjectStatus
diff --git a/ymir/web/src/hooks/usePublish.ts b/ymir/web/src/hooks/usePublish.ts
new file mode 100644
index 0000000000..666775c4bc
--- /dev/null
+++ b/ymir/web/src/hooks/usePublish.ts
@@ -0,0 +1,59 @@
+import { message } from 'antd'
+import { useEffect, useState } from 'react'
+import { getLocale, useSelector } from 'umi'
+
+import { getDeployUrl } from '@/constants/common'
+import { ModelVersion } from '@/interface/model'
+import t from '@/utils/t'
+
+const base = getDeployUrl()
+const id = 'publishIframe'
+
+const createIframe = (params = {}) => {
+ const url = `${base}/postModelMsg?data=${encodeURIComponent(JSON.stringify(params))}`
+ let iframe = document.createElement('iframe')
+ document.body.appendChild(iframe)
+ iframe.id = id
+ iframe.style.position = 'absolute'
+ iframe.style.top = '-1000px'
+ iframe.style.left = '-1000px'
+ iframe.src = url
+ return iframe
+}
+
+const usePublish = () => {
+ const [result, setResult] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const { id: userId, username: userName } = useSelector((state: { user: any }) => state.user)
+
+ const publish = (data: ModelVersion) => {
+ const key = 'publish'
+ if (loading) {
+ return
+ }
+ setLoading(true)
+ message.loading({ content: t('algo.publish.tip.loading'), key })
+
+ const lang = getLocale()
+ const url = window.location.origin + data.url
+ const stage = data.stages?.find(stg => stg.id === data.recommendStage)?.name
+ const params = {
+ lang, userId, userName,
+ modelId: data.id,
+ modelName: `${data.name} ${data.versionName}`,
+ stage,
+ url,
+ }
+ // create iframe
+ console.log('publish params:', params)
+ createIframe(params)
+ setTimeout(() => {
+ setLoading(false)
+ message.success({ content: t('algo.publish.tip.success'), key })
+ }, 2000)
+ }
+
+ return [publish, result]
+}
+
+export default usePublish
diff --git a/ymir/web/src/hooks/useRerunAction.tsx b/ymir/web/src/hooks/useRerunAction.tsx
new file mode 100644
index 0000000000..4242ecbab6
--- /dev/null
+++ b/ymir/web/src/hooks/useRerunAction.tsx
@@ -0,0 +1,39 @@
+import { useHistory } from "umi"
+import t from "@/utils/t"
+import { TASKTYPES } from '@/constants/task'
+import { RefreshIcon } from "@/components/common/icons"
+import { Result } from "@/interface/common"
+import { Button } from "antd"
+
+export default function useRerunAction(mode = 'menu') {
+ const history = useHistory()
+
+ const rerun = (pid: number, type: string, record: Result) => {
+ history.push({ pathname: `/home/project/${pid}/${type}`, state: { record } })
+ }
+
+ const generateRerunAction = (record: Result) => {
+ const maps = {
+ [TASKTYPES.TRAINING]: 'train',
+ [TASKTYPES.MINING]: 'mining',
+ [TASKTYPES.INFERENCE]: 'inference',
+ }
+ const type = maps[record.taskType]
+ const pid = record.projectId
+ const renderMenu = type ? {
+ key: "rerun",
+ label: t(`common.action.rerun.${type}`),
+ onclick: () => rerun(pid, type, record),
+ icon: ,
+ } : { hidden: () => true }
+
+ const renderBtn = type ? rerun(pid, type, record)}
+ >
+ {t(`common.action.rerun.${type}`)}
+ : null
+ return mode === 'btn' ? renderBtn : renderMenu
+ }
+ return generateRerunAction
+}
\ No newline at end of file
diff --git a/ymir/web/src/hooks/useUpdateProject.js b/ymir/web/src/hooks/useUpdateProject.js
new file mode 100644
index 0000000000..b2881f72e5
--- /dev/null
+++ b/ymir/web/src/hooks/useUpdateProject.js
@@ -0,0 +1,22 @@
+import { useState } from 'react'
+import { useDispatch } from 'umi'
+
+const useUpdateProject = (id) => {
+ const dispatch = useDispatch()
+ const updateProject = payload => dispatch({
+ type: 'project/updateProject',
+ payload,
+ })
+ const [updated, setUpdated] = useState({})
+
+ const update = async (params = {}) => {
+ const result = await updateProject({ id, ...params })
+ if (result) {
+ setUpdated(result)
+ }
+ }
+
+ return [updated, update]
+}
+
+export default useUpdateProject
diff --git a/ymir/web/src/hooks/useWindowResize.js b/ymir/web/src/hooks/useWindowResize.js
new file mode 100644
index 0000000000..5f15cc1641
--- /dev/null
+++ b/ymir/web/src/hooks/useWindowResize.js
@@ -0,0 +1,16 @@
+import { useEffect, useState } from "react"
+
+const useWindowResize = () => {
+ const [width, setWidth] = useState(window.innerWidth)
+ const resizeHandle = () => setWidth(window.innerWidth)
+ useEffect(() => {
+ window.addEventListener('resize', resizeHandle)
+ return () => {
+ window.removeEventListener('resize', resizeHandle)
+ }
+ }, [])
+
+ return width
+}
+
+export default useWindowResize
diff --git a/ymir/web/src/interface/common.ts b/ymir/web/src/interface/common.ts
index 9e01408c34..62189ed60a 100644
--- a/ymir/web/src/interface/common.ts
+++ b/ymir/web/src/interface/common.ts
@@ -27,4 +27,5 @@ export interface Result {
project?: Project,
task?: Task,
hidden: boolean,
+ description: string,
}
diff --git a/ymir/web/src/interface/dataset.ts b/ymir/web/src/interface/dataset.ts
index 05a34e2c69..9a9ed8f5dc 100644
--- a/ymir/web/src/interface/dataset.ts
+++ b/ymir/web/src/interface/dataset.ts
@@ -1,8 +1,26 @@
-import { Result } from "@/interface/common"
+import { Result, BackendData } from "@/interface/common"
type Keywords = {
[key: string]: number,
}
+type CK = {
+ [key: string]: any,
+}
+type AnnotationsCount = {
+ count: Keywords,
+ keywords: Array,
+ negative: number,
+ total: number,
+}
+type AnylysisAnnotation = {
+ keywords: Keywords,
+ total: number,
+ average: number,
+ negative: number,
+ quality: Array,
+ area: Array,
+ areaRatio: Array,
+}
export interface DatasetGroup {
id: number,
name: string,
@@ -13,10 +31,60 @@ export interface DatasetGroup {
export interface Dataset extends Result {
keywordCount: number,
- keywordsCount: Keywords,
isProtected: Boolean,
- nagetiveCount?: number,
- projectNagetiveCount?: number,
assetCount: number,
- ignoredKeywords: Array,
+ gt?: AnnotationsCount,
+ pred?: AnnotationsCount,
+ inferClass?: Array,
+ cks?: BackendData,
+ tags?: BackendData,
+}
+
+export interface DatasetAnalysis {
+ name: string,
+ version: number,
+ versionName: string,
+ assetCount: number,
+ totalAssetMbytes: number,
+ assetBytes: Array,
+ assetHWRatio: Array,
+ assetArea: Array,
+ assetQuality: Array,
+ gt: AnylysisAnnotation,
+ pred: AnylysisAnnotation,
+ inferClass?: Array,
+ cks?: BackendData,
+ tags?: BackendData,
+}
+
+export interface Asset {
+ id: number,
+ hash: string,
+ keywords: Array,
+ url: string,
+ metadata?: {
+ width: number,
+ height: number,
+ channel: number,
+ },
+ size?: number,
+ annotations: Array,
+ evaluated?: boolean,
+ cks?: CK,
+}
+
+export interface Annotation {
+ keyword: string,
+ box: {
+ x: number,
+ y: number,
+ w: number,
+ h: number,
+ rotate_angle: number,
+ }
+ color?: string,
+ score?: number,
+ gt?: boolean,
+ cm: number,
+ tags?: CK,
}
diff --git a/ymir/web/src/interface/image.ts b/ymir/web/src/interface/image.ts
new file mode 100644
index 0000000000..a4044c2f47
--- /dev/null
+++ b/ymir/web/src/interface/image.ts
@@ -0,0 +1,19 @@
+export interface InferConfig {
+ type: number,
+ config: {
+ [key: string]: any,
+ },
+}
+export interface Image {
+ id: number,
+ name: string,
+ state: number,
+ isShared: boolean,
+ functions:Array,
+ configs: Array,
+ url: string,
+ liveCode?: boolean,
+ description: string,
+ createTime: string,
+ related?: Array,
+}
diff --git a/ymir/web/src/interface/iteration.ts b/ymir/web/src/interface/iteration.ts
new file mode 100644
index 0000000000..89b24218e3
--- /dev/null
+++ b/ymir/web/src/interface/iteration.ts
@@ -0,0 +1,18 @@
+type DatasetId = number
+
+export interface Iteration {
+ id: number,
+ projectId: number,
+ name?: string,
+ round: number,
+ currentStage: number,
+ testSet?: DatasetId,
+ trainSet?: DatasetId,
+ trainUpdateSet: DatasetId,
+ wholeMiningSet: DatasetId,
+ miningSet?: DatasetId,
+ miningResult?: DatasetId,
+ labelSet?: DatasetId,
+ model?: number,
+ prevIteration: number,
+}
diff --git a/ymir/web/src/interface/model.ts b/ymir/web/src/interface/model.ts
index 719ceb8f4f..70ed870943 100644
--- a/ymir/web/src/interface/model.ts
+++ b/ymir/web/src/interface/model.ts
@@ -1,5 +1,12 @@
import { Result } from "@/interface/common"
+export interface Stage {
+ id: number,
+ name: string,
+ map: number,
+ modelId?: number,
+ modelName?: string,
+}
export interface ModelGroup {
id: number,
projectId: number,
@@ -9,4 +16,6 @@ export interface ModelGroup {
export interface ModelVersion extends Result {
map: number,
url: string,
+ stages?: Array,
+ recommendStage: number,
}
diff --git a/ymir/web/src/interface/project.ts b/ymir/web/src/interface/project.ts
index 423a5b0a77..86eee6b0b8 100644
--- a/ymir/web/src/interface/project.ts
+++ b/ymir/web/src/interface/project.ts
@@ -1,17 +1,19 @@
import { DatasetGroup, Dataset } from "@/interface/dataset"
-import { ModelVersion as Model } from "@/interface/model"
-type DatasetId = number
+import { Iteration } from './iteration'
export interface Project {
id: number,
name: string,
type: number,
keywords: Array,
+ candidateTrainSet: number,
trainSet?: DatasetGroup,
testSet?: Dataset,
miningSet?: Dataset,
+ testingSets?: Array,
setCount: number,
trainSetVersion?: number,
model?: number,
+ modelStage?: Array,
modelCount: number,
miningStrategy: number,
chunkSize?: number,
@@ -24,25 +26,8 @@ export interface Project {
isExample?: boolean,
hiddenDatasets: Array,
hiddenModels: Array,
-}
-
-export interface Iteration {
- id: number,
- projectId: number,
- name?: string,
- round: number,
- currentStage: number,
- testSet?: DatasetId,
- trainSet?: DatasetId,
- trainUpdateSet: DatasetId,
- trainUpdateDataset?: Dataset,
- miningSet?: DatasetId,
- miningDataset?: Dataset,
- miningResult?: DatasetId,
- miningResultDataset?: Dataset,
- labelSet?: DatasetId,
- labelDataset?: Dataset,
- model?: number,
- trainingModel?: Model,
- prevIteration: number,
+ enableIteration: boolean,
+ totalAssetCount: number,
+ runningTaskCount: number,
+ totalTaskCount: number,
}
diff --git a/ymir/web/src/layouts/common.less b/ymir/web/src/layouts/common.less
index 9e753b740f..c5569d24b0 100644
--- a/ymir/web/src/layouts/common.less
+++ b/ymir/web/src/layouts/common.less
@@ -3,7 +3,7 @@
}
.home {
.content {
- margin: 0 5vw;
+ margin: 0 20px;
}
.sider {
// background-color: #f4f4f4;
diff --git a/ymir/web/src/layouts/index.js b/ymir/web/src/layouts/index.js
index 62a313d6a9..7b95230843 100644
--- a/ymir/web/src/layouts/index.js
+++ b/ymir/web/src/layouts/index.js
@@ -5,10 +5,10 @@ import React, { useEffect } from "react"
import { ConfigProvider, Layout, message } from "antd"
import Loading from "@/components/common/loading"
import Foot from "@/components/common/footer"
+import LeftMenu from "@/components/common/leftMenu"
import Empty from '@/components/empty/default'
import '@/assets/icons/iconfont.css'
-import QuickActions from "@/components/common/quickActions"
-import Guide from "@/components/guide/guide"
+import { withRouter } from "umi"
const { Header, Content, Sider, Footer } = Layout
message.config({ maxCount: 1 })
@@ -16,8 +16,6 @@ message.config({ maxCount: 1 })
function BasicLayout(props) {
let { logined, history } = props
useEffect(() => {
- // console.log("comp use effect: ", `logined: ${logined}`)
- // console.log("history from layout", history)
if (!logined) {
history.replace(`/login?redirect=${history.location.pathname}`)
return
@@ -36,24 +34,22 @@ function BasicLayout(props) {
- {/*
-
-
- */}
-
- {props.children}
-
-
+
+
+
+ {props.children}
+
+
+
- {/* */}
{/*
*/}
@@ -75,4 +71,4 @@ const mapDispatchToProps = (dispatch) => {
},
}
}
-export default connect(mapStateToProps, mapDispatchToProps)(BasicLayout)
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(BasicLayout))
diff --git a/ymir/web/src/locales/langModules.ts b/ymir/web/src/locales/langModules.ts
index 499652c5c8..5965bd589d 100644
--- a/ymir/web/src/locales/langModules.ts
+++ b/ymir/web/src/locales/langModules.ts
@@ -14,6 +14,7 @@ import user from './modules/user'
import image from './modules/image'
import tip from './modules/tip'
import project from './modules/project'
+import algo from './modules/algo'
export default {
...common,
@@ -31,4 +32,5 @@ export default {
...image,
...tip,
...project,
+ ...algo,
}
diff --git a/ymir/web/src/locales/modules/algo.ts b/ymir/web/src/locales/modules/algo.ts
new file mode 100644
index 0000000000..0746334f1e
--- /dev/null
+++ b/ymir/web/src/locales/modules/algo.ts
@@ -0,0 +1,12 @@
+const algo = {
+ 'algo.title': { cn: '公共算法', en: 'Public Algorithm', },
+ 'algo.label': { cn: '模型部署', en: 'Model Deployment', },
+ 'algo.public.label': { cn: '公共算法', en: 'Public Algorithm', },
+ 'algo.mine.label': { cn: '我的算法', en: 'My Algorithm', },
+ 'algo.device.label': { cn: '设备列表', en: 'Devices', },
+ 'algo.support.label': { cn: '支持设备', en: 'Supported Devices', },
+ 'algo.publish.tip.loading': { cn: '发布中...', en: 'Loading...', },
+ 'algo.publish.tip.success': { cn: '模型正在发布中,请耐心等待,可到我的算法查看发布结果', en: 'Publishing, please wait. You can turn to My Algorithm to view more', },
+}
+
+export default algo
diff --git a/ymir/web/src/locales/modules/breadcrumbs.ts b/ymir/web/src/locales/modules/breadcrumbs.ts
index 5003f09e06..441ed97626 100644
--- a/ymir/web/src/locales/modules/breadcrumbs.ts
+++ b/ymir/web/src/locales/modules/breadcrumbs.ts
@@ -2,28 +2,32 @@ const breadcrumbs = {
'breadcrumbs.portal': { cn: '首页', en: 'Home', },
'breadcrumbs.models': { cn: '模型列表', en: 'Model List', },
'breadcrumbs.datasets': { cn: '数据集列表', en: 'Dataset List', },
- 'breadcrumbs.task.fusion': { cn: '数据预处理', en: 'Data Pretreatment', },
+ 'breadcrumbs.task.fusion': { cn: '挖掘数据准备', en: 'Data Pretreatment', },
+ 'breadcrumbs.task.merge': { cn: '数据合并', en: 'Data Merge', },
+ 'breadcrumbs.task.filter': { cn: '数据筛选', en: 'Data Filter', },
'breadcrumbs.task.training': { cn: '模型训练', en: 'Model Training', },
'breadcrumbs.task.label': { cn: '标注数据', en: 'Dataset Labeling', },
'breadcrumbs.task.mining': { cn: '挖掘数据', en: 'Dataset Mining', },
'breadcrumbs.task.inference': { cn: '数据推理', en: 'Dataset Inference', },
'breadcrumbs.datasets.copy': { cn: '复制数据集', en: 'Dataset Copy', },
'breadcrumbs.dataset': { cn: '数据集详情', en: 'Dataset Detail', },
- 'breadcrumbs.dataset.add': { cn: '导入数据集', en: 'Import Dataset', },
+ 'breadcrumbs.dataset.add': { cn: '添加数据集', en: 'Add Dataset', },
+ 'breadcrumbs.dataset.analysis': { cn: '数据集分析', en: 'Dataset Analysis', },
'breadcrumbs.dataset.assets': { cn: '数据列表', en: 'Assets List', },
'breadcrumbs.dataset.asset': { cn: '数据详情', en: 'Asset Detail', },
- 'breadcrumbs.dataset.compare': { cn: '数据集比对', en: 'Dataset Comparison', },
'breadcrumbs.model.add': { cn: '导入模型', en: 'Model Import', },
'breadcrumbs.model': { cn: '模型详情', en: 'Model Detail', },
+ 'breadcrumbs.model.diagnose': { cn: '模型诊断', en: 'Model Diagnose', },
'breadcrumbs.model.verify': { cn: '模型验证', en: 'Model Verification', },
'breadcrumbs.history': { cn: '历史树', en: 'History Tree', },
- 'breadcrumbs.keyword': { cn: '标签管理', en: 'Keyword List', },
+ 'breadcrumbs.keyword': { cn: '类别管理', en: 'Classes', },
'breadcrumbs.configure.permission': { cn: '权限配置', en: 'Permission', },
- 'breadcrumbs.user.info': { cn: '用户管理', en: 'User Information', },
- 'breadcrumbs.images': { cn: '镜像列表', en: 'Images', },
- 'breadcrumbs.image': { cn: '镜像详情', en: 'Image', },
+ 'breadcrumbs.user.info': { cn: '个人中心', en: 'User', },
+ 'breadcrumbs.images': { cn: '我的镜像', en: 'My Images', },
+ 'breadcrumbs.image': { cn: '镜像详情', en: 'Image Detail', },
'breadcrumbs.projects': { cn: '项目管理', en: 'Project', },
'breadcrumbs.project': { cn: '项目详情', en: 'Detail', },
+ 'breadcrumbs.project.summary': { cn: '项目概览', en: 'Summary', },
'breadcrumbs.project.add': { cn: '创建项目', en: 'Create', },
'breadcrumbs.project.edit': { cn: '项目设置', en: 'Settings', },
'breadcrumbs.project.iterations': { cn: '迭代列表', en: 'Iterations', },
diff --git a/ymir/web/src/locales/modules/common.ts b/ymir/web/src/locales/modules/common.ts
index aab3366939..841b241a58 100644
--- a/ymir/web/src/locales/modules/common.ts
+++ b/ymir/web/src/locales/modules/common.ts
@@ -1,13 +1,13 @@
const common = {
'common.top.menu.logout': { cn: '退出', en: 'Logout', },
- 'common.top.menu.user': { cn: '用户管理', en: 'User', },
+ 'common.top.menu.user': { cn: '个人中心', en: 'User', },
'common.top.menu.community': { cn: '开源社区', en: 'GitHub', },
- 'common.top.menu.home': { cn: "主页", en: "Home", },
+ 'common.top.menu.home': { cn: "首页", en: "Home", },
'common.top.menu.project': { cn: "项目管理", en: "Project", },
- 'common.top.menu.keyword': { cn: "标签管理", en: "Keyword", },
+ 'common.top.menu.keyword': { cn: "类别管理", en: "Classes", },
'common.top.menu.configure': { cn: "系统配置", en: "Configure", },
'common.top.menu.permission': { cn: "权限配置", en: "Permission", },
- 'common.top.menu.image': { cn: "镜像列表", en: "Images", },
+ 'common.top.menu.image': { cn: "我的镜像", en: "My Images", },
'common.history.node.title': { cn: '节点详情', en: 'Node Detail', },
'common.back': { cn: '返回', en: 'Back', },
'common.editbox.action.edit': { cn: '编辑名称', en: 'Edit Name', },
@@ -35,7 +35,7 @@ const common = {
'common.qa.action.import': {cn: '导入{br}数据集', en: 'Import Dataset', },
'common.qa.action.train': {cn: '训练{br}模型', en: 'Train Model', },
'common.qa.action.guide': {cn: '操作{br}指引', en: 'Guide', },
- 'common.empty.keywords': {cn: '无标签', en: 'None', },
+ 'common.empty.keywords': {cn: '无类别', en: 'None', },
'common.modify': {cn: '修改', en: 'Modify', },
'common.all': {cn: '全部', en: 'All', },
'common.yes': {cn: '是', en: 'Yes', },
@@ -49,34 +49,46 @@ const common = {
'common.del': {cn: '删除', en: 'Delete', },
'common.key': {cn: '键', en: 'Key', },
'common.value': {cn: '值', en: 'Value', },
- 'common.recommend.keyword.label': {cn: '常用标签', en: 'Recommend Keywords', },
- 'common.index.keyword.label': {cn: '标签', en: 'Keywords', },
+ 'common.recommend.keyword.label': {cn: '常用类别', en: 'Recommend Classes', },
+ 'common.index.keyword.label': {cn: '类别', en: 'Classes', },
'common.view': {cn: '查看', en: 'View', },
'common.step.next': {cn: '下一步', en: 'Next', },
'common.skip': {cn: '跳过', en: 'Skip', },
'common.confirm': {cn: '确定', en: 'OK', },
+ 'common.cancel': {cn: '取消', en: 'Cancel', },
'common.done': {cn: '已完成', en: 'Done', },
'common.action.train': {cn: '训练', en: 'Train', },
- 'common.action.mine': {cn: '挖掘', en: 'Mine', },
+ 'common.action.mining': {cn: '挖掘', en: 'Mine', },
'common.action.fusion': {cn: '预处理', en: 'Pretreat', },
+ 'common.action.merge': {cn: '添加', en: 'Add', },
+ 'common.action.filter': {cn: '筛选', en: 'Filter', },
'common.action.label': {cn: '标注', en: 'Label', },
- 'common.action.infer': {cn: '推理', en: 'Infer', },
+ 'common.action.inference': {cn: '推理', en: 'Infer', },
'common.action.copy': {cn: '复制', en: 'Copy', },
- 'common.action.import': {cn: '导入', en: 'Import', },
+ 'common.action.import': {cn: '添加', en: 'Add', },
'common.action.hide': {cn: '隐藏', en: 'Hide', },
- 'common.action.compare': {cn: '比对', en: 'Compare', },
- 'common.action.multiple.compare': {cn: '批量比对', en: 'Batch Compare', },
'common.action.check': {cn: '检查', en: 'Check', },
'common.action.check.again': {cn: '重新检查', en: 'Recheck', },
'common.action.multiple.hide': {cn: '批量隐藏', en: 'Batch Hide', },
'common.action.multiple.restore': {cn: '批量显示', en: 'Batch Show', },
'common.action.multiple.infer': {cn: '批量推理', en: 'Batch Inference', },
+ 'common.action.multiple.merge': {cn: '批量合并', en: 'Batch Merge', },
+ 'common.action.rerun.train': {cn: '再次训练', en: 'ReTrain', },
+ 'common.action.rerun.mining': {cn: '再次挖掘', en: 'ReMine', },
+ 'common.action.rerun.inference': {cn: '再次推理', en: 'ReInfer', },
'common.action.restore': {cn: '显示', en: 'Show', },
+ 'common.action.diagnose.training': {cn: '训练过程诊断', en: 'Training Diagnose', },
'common.hidden.label': {cn: '显示状态', en: 'Visible Status', },
'common.state.hidden': {cn: '隐藏', en: 'Hidden', },
'common.selected.required': {cn: '需要选中至少一项', en: 'Selected required', },
'common.hidden.list': {cn: '隐藏列表', en: 'Hidden List', },
- 'common.everage': {cn: '平均值', en: 'Everage', },
+ 'common.average': {cn: '平均值', en: 'Average', },
+ 'common.recommend': {cn: '推荐', en: 'Recommend', },
+ "common.desc": { en: "Description", cn: "描述", },
+ "common.reset": { en: "Reset", cn: "重置", },
+ "common.menu.docs": { en: "Documents", cn: "说明文档", },
+ "annotation.gt": { en: "GT", cn: "标准值", },
+ "annotation.pred": { en: "Prediction", cn: "预测标注", },
}
export default common
diff --git a/ymir/web/src/locales/modules/dataset.ts b/ymir/web/src/locales/modules/dataset.ts
index 5b2550ccd6..213ec8e069 100644
--- a/ymir/web/src/locales/modules/dataset.ts
+++ b/ymir/web/src/locales/modules/dataset.ts
@@ -1,19 +1,20 @@
const dataset = {
+ "dataset.list": { cn: "数据集管理", en: "Datasets", },
"dataset.detail.title": { cn: "数据集详情", en: "Dataset Detail", },
"dataset.asset.title": { cn: "数据详情", en: "Dataset Assets", },
"dataset.state.ready": { cn: "进行中", en: "Running", },
"dataset.state.valid": { cn: "可用", en: "Valid", },
"dataset.state.invalid": { cn: "不可用", en: "Invalid", },
- "dataset.column.name": { cn: "名称", en: "Dataset Name", },
- "dataset.column.source": { cn: "来源", en: "Source", },
+ "dataset.column.name": { cn: "版本", en: "Version", },
+ "dataset.column.source": { cn: "数据集来源", en: "Dataset Source", },
"dataset.column.asset_count": { cn: "图片数", en: "Assets' Count", },
- "dataset.column.keyword": { cn: "标签", en: "Keywords", },
- "dataset.column.ignored_keyword": { cn: "忽略标签", en: "Ignored Keywords", },
+ "dataset.column.keyword": { cn: "类别", en: "Classes", },
+ "dataset.column.ignored_keyword": { cn: "忽略类别", en: "Ignored Classes", },
"dataset.column.state": { cn: "状态", en: "Status", },
"dataset.column.create_time": { cn: "创建时间", en: "Create Time", },
"dataset.column.hidden_time": { cn: "隐藏时间", en: "Hidden Time", },
"dataset.column.model": { cn: "模型名称", en: "Model Name", },
- "dataset.column.map": { cn: "AP(平均IoU)", en: "AP(Average of IoU)", },
+ "dataset.column.map": { cn: "mAP(平均IoU)", en: "mAP(Average of IoU)", },
"dataset.column.action": { cn: "操作", en: "Actions", },
"dataset.column.keyword.label": { cn: "{keywords} 共{total}个", en: "{keywords} total {total}.", },
"dataset.action.fusion": { cn: "预处理", en: "Pretreat", },
@@ -25,7 +26,7 @@ const dataset = {
"dataset.action.edit": { cn: "编辑", en: "Rename", },
'dataset.action.inference': { cn: '推理', en: 'Inference', },
"dataset.empty.label": { cn: "去导入一个数据集", en: "Import A Dataset", },
- "dataset.import.label": { cn: "导入数据集", en: "Import Dataset", },
+ "dataset.import.label": { cn: "添加数据集", en: "Add Dataset", },
"dataset.query.name": { cn: "名称", en: "Dataset Name", },
"dataset.action.del.confirm.content": { cn: "确认要删除数据集版本:{name}?", en: "Are you sure to remove this dataset version:{name}?", },
"dataset.action.hide.confirm.content": { cn: "确认要隐藏数据集版本:{name}?", en: "Are you sure to hide dataset versions: {name}?", },
@@ -40,9 +41,10 @@ const dataset = {
},
"dataset.query.name.placeholder": { cn: "数据集名称", en: "Dataset Name", },
"dataset.detail.pager.total": { cn: "共 {total} 图像", en: "Total {total} Pictures", },
- "dataset.detail.keyword.label": { cn: "标签:", en: "Keywords: ", },
+ "dataset.detail.keyword.label": { cn: "类别:", en: "Classes: ", },
"dataset.detail.randompage.label": { cn: "随机页", en: "Random Page", },
- "dataset.detail.assets.keywords.total": { cn: "共{total}个标签", en: "{total} keywords", },
+ "dataset.detail.assets.keywords.total": { cn: "共{total}个类别", en: "{total} classes", },
+ "dataset.asset.filters.title": { cn: "评估结果:(重叠度: 0.5, 置信度: 0.8)", en: "Evaluation (IoU: 0.5, Confidence: 0.8)", },
"dataset.asset.info": { cn: "数据信息", en: "Asset Info", },
"dataset.asset.info.id": { cn: "标识", en: "ID", },
"dataset.asset.info.size": { cn: "大小", en: "Size", },
@@ -50,7 +52,7 @@ const dataset = {
"dataset.asset.info.height": { cn: "高", en: "Height", },
"dataset.asset.info.channel": { cn: "通道", en: "Channels", },
"dataset.asset.info.timestamp": { cn: "时间戳", en: "Timestamp", },
- "dataset.asset.info.keyword": { cn: "标签", en: "Keywords", },
+ "dataset.asset.info.keyword": { cn: "类别", en: "Classes", },
"dataset.asset.random": { cn: "随机图像", en: "Random Asset", },
"dataset.asset.back": { cn: "上一个", en: "Previous Asset", },
"dataset.asset.empty": { cn: "查询不到指定asset", en: "Invalid Asset", },
@@ -62,33 +64,39 @@ const dataset = {
"dataset.add.types.local": { cn: "本地导入", en: "Local Import", },
"dataset.add.types.path": { cn: "路径导入", en: "Path Import", },
"dataset.add.success.msg": { cn: "导入正在进行中", en: "Dataset Importing", },
- "dataset.add.form.name.label": { cn: "名称", en: "Name", },
- "dataset.add.form.name.required": { cn: "请输入数据集名称", en: "Dataset Name", },
- "dataset.add.form.type.label": { cn: "导入类型", en: "Type", },
- "dataset.add.form.label.label": { cn: "标注状态", en: "Labeling Status", },
+ "dataset.add.form.name.label": { cn: "数据集名称", en: "Dataset Name", },
+ "dataset.add.form.name.required": { cn: "数据集名称为必填项", en: "Dataset Name Required", },
+ "dataset.add.form.name.placeholder": { cn: "请输入数据集名称,支持2-80个字符", en: "Please input dataset name, 2 - 80 characters", },
+ "dataset.add.form.type.label": { cn: "添加类型", en: "Type", },
+ "dataset.add.form.label.label": { cn: "标注", en: "Labeling Status", },
"dataset.add.form.newkw.label": { cn: " ", en: " ", },
- "dataset.add.newkw.asname": { cn: "添加标签", en: "As Keyword", },
+ "dataset.add.newkw.asname": { cn: "添加类别", en: "As Class", },
"dataset.add.newkw.asalias": { cn: "添加为别名", en: "As Alias", },
- "dataset.add.newkw.ignore": { cn: "忽略此标签", en: "Ignore", },
- "dataset.add.form.newkw.link": { cn: "前往标签列表添加>>", en: "Go to the keyword list to add>>", },
+ "dataset.add.newkw.ignore": { cn: "忽略此类别", en: "Ignore", },
+ "dataset.add.form.newkw.link": { cn: "前往类别管理添加>>", en: "Go to the class manegement to add>>", },
"dataset.add.form.newkw.tip": {
- cn: "当导入模型的标签内容不在当前的用户标签列表时,选择导入策略。",
+ cn: "当导入模型的类别内容不在当前的用户类别列表时,选择导入策略。",
en: "Select an import policy when the tag of the imported dataset does not belong to the current list of user tags.",
},
- "dataset.add.label_strategy.include": { cn: "包含标注信息", en: "Contains Annotations", },
- "dataset.add.label_strategy.exclude": { cn: "不包含标注信息", en: "No Annotations", },
- "dataset.add.label_strategy.ignore": { cn: "忽略新标签和对应标注", en: "Ignore unknown keywords and annotations", },
- "dataset.add.label_strategy.add": { cn: "添加到标签列表", en: "Add Keywords to your Keywords List", },
- "dataset.add.label_strategy.stop": { cn: "终止数据集导入", en: "Terminate dataset import", },
+ "dataset.add.label_strategy.exclude": { cn: "不包含标注", en: "Only Assets", },
+ "dataset.add.label_strategy.ignore": { cn: "只添加已有类别的标注", en: "Ignore unknown classes and annotations", },
+ "dataset.add.label_strategy.add": { cn: "添加所有标注", en: "Add Classes", },
"dataset.add.form.internal.label": { cn: "数据集", en: "Dataset", },
"dataset.add.form.internal.required": { cn: "请选择公共数据集", en: "Please select public dataset", },
"dataset.add.form.internal.placeholder": { cn: "请选择一个公共数据集", en: "Select A Public Dataset", },
"dataset.add.form.net.label": { cn: "URL地址", en: "URL", },
- "dataset.add.form.net.tip": { cn: "请输入压缩文件的url地址", en: "Please input a url of zip file", },
+ "dataset.add.form.net.placeholder": { cn: "请输入压缩文件的url地址", en: "Please input a url of zip file", },
"dataset.add.form.path.label": { cn: "相对路径", en: "Relative Path", },
+ "dataset.add.form.tip.format.detail": { cn: "查看标注格式(.xml)及meta.yaml格式", en: "View more about annotation format or meta.yaml", },
+ "dataset.add.form.tip.structure": {
+ cn: "图片文件需放入images文件夹内,标准值标注文件需放入gt文件夹内,模型推理标注文件需放入pred文件夹内。gt和pred都是可选的。文件结构如下:{br}{pic}{br}{detail}",
+ en: "image -> images; gt -> GT annotations; pred -> predictions. gt and pred is optional. structure: {br}{pic}"
+ },
"dataset.add.form.path.tip": {
- cn: "将数据文件夹存放到ymir工作空间目录下的ymir-sharing目录,如 /home/ymir/ymir-workspace/ymir-sharing/VOC2012, 输入基于ymir-sharing相对路径:VOC2012",
- en: "Save the data in 'ymir-sharing' under ymir workspace directory, such as /home/ymir/ymir-workspace/ymir-sharing/VOC2012, and input relative path base on ymir-sharing: VOC2012",
+ cn: `1. 将数据文件夹存放到ymir工作空间目录下的ymir-sharing目录,如 /home/ymir/ymir-workspace/ymir-sharing/VOC2012, 输入基于ymir-sharing相对路径:VOC2012{br}
+ 2. {structure}`,
+ en: `1. Save the data in 'ymir-sharing' under ymir workspace directory, such as /home/ymir/ymir-workspace/ymir-sharing/VOC2012, and input relative path base on ymir-sharing: VOC2012{br}
+ 2. {structure}`,
},
"dataset.add.form.path.placeholder": { cn: "请输入路径", en: "Please input path on server", },
"dataset.add.form.upload.btn": { cn: "上传文件", en: "Upload", },
@@ -96,40 +104,73 @@ const dataset = {
cn: `1. 仅支持zip格式压缩包文件上传;{br}
2. 局域网内压缩包大小 < 1G, 互联网建议 < 200MB;{br}
3. 压缩包内图片格式要求为:图片格式为*.jpg、*.jpeg、*.png、*.bmp,格式不符的图片将不会导入,标注文件格式为Pascal VOC。{br}
- 4. 压缩包文件内图片文件需放入images文件夹内,标注文件需放入annotations文件夹内,如以下示例:{sample}{br}
- 5. 压缩包内文件结构如下:{br}{pic}`,
+ 4. 示例:{sample}{br}
+ 5. {structure}`,
en: `1. Only zip file allowed;{br}
2. Size < 1G;{br}
3. Images format allowed *.jpg, *.jpeg, *.png, *.bmp, images with unmatched format can not be imported, annotations format supported pascal(*.xml){br}
4. Sample: {sample}{br}
- 5. zip structure: {br}{pic}`
+ 5. {structure}`
+ },
+ "dataset.add.form.net.tip": {
+ cn: `1. 示例: https://www.examples.com/pascal.zip{br}
+ 2. {structure}`,
+ en: `1. Sample: https://www.examples.com/pascal.zip{br}
+ 2. {structure}`
},
"dataset.copy.form.dataset": { cn: "原数据集", en: "Original Dataset", },
"dataset.copy.form.desc.label": { cn: '备注', en: 'Description', },
"dataset.copy.success.msg": { cn: "数据集正在复制,请稍等", en: "Dataset copying", },
- 'dataset.detail.action.fusion': { cn: '数据预处理', en: 'Data Pretreatment', },
+ 'dataset.detail.action.fusion': { cn: '挖掘数据准备', en: 'Data Pretreatment', },
'dataset.detail.action.train': { cn: '训练模型', en: 'Train Model', },
'dataset.detail.action.mining': { cn: '挖掘数据', en: 'Mining', },
'dataset.detail.action.label': { cn: '数据标注', en: 'Label', },
- 'dataset.import.public.include': { cn: '包含标签', en: 'Include', },
- 'dataset.add.newkeyword.empty': { cn: '无新标签', en: 'None of new keywords', },
+ 'dataset.import.public.include': { cn: '添加新类别', en: 'New Classes', },
+ 'dataset.add.newkeyword.empty': { cn: '无新类别', en: 'None of new classes', },
'dataset.add.local.file.empty': { cn: '请上传本地文件', en: 'Please upload a zip file', },
'dataset.samples.negative': { cn: '负样本', en: 'Negative Samples', },
- 'dataset.train.form.samples': { cn: '样本比率', en: 'Samples Rates', },
+ 'dataset.train.form.samples': { cn: '正负样本', en: 'Neg./Pos. Samples', },
'dataset.detail.label.name': { cn: '数据集名称', en: 'Dataset Name', },
'dataset.detail.label.assets': { cn: '图片数', en: 'Assets Count', },
- 'dataset.detail.label.keywords': { cn: '标签', en: 'Keywords', },
+ 'dataset.detail.label.keywords': { cn: '类别', en: 'Classes', },
'dataset.add.form.copy.label': { cn: '源数据集', en: 'Original Dataset', },
'dataset.add.form.copy.required': { cn: '源数据集不能为空', en: 'Original dataset is required', },
'dataset.add.form.copy.placeholder': { cn: '请选择待复制的数据集版本', en: 'Select a dataset version for copy', },
'dataset.add.validate.url.invalid': { cn: '不是合法的网络地址', en: 'Invalid url', },
- 'dataset.compare.error.diff_group': { cn: '比对的版本必须在同一个数据集', en: 'Target versions must be in one dataset', },
- 'dataset.compare.error.diff_assets': { cn: '比对的数据集版本数据量需要保持一致', en: 'Assets\' count must be the same for every version', },
- 'dataset.compare.form.datasets': { cn: '比对数据集', en: 'Compare Datasets', },
- 'dataset.compare.form.gt': { cn: '真值(Ground Truth)', en: 'Ground Truth', },
- 'dataset.compare.form.confidence': { cn: '置信度', en: 'Confidence', },
- 'dataset.compare.restart': { cn: '重新比对', en: 'Compare Again', },
- 'dataset.fusion.validate.inputs': { cn: '请输入至少一项预处理条件', en: 'Please input at less one condition for pretreating', },
+ 'dataset.fusion.validate.inputs': { cn: '请输入至少一项预处理条件', en: 'Please input one condition at least for pretreating', },
+ 'dataset.filter.validate.inputs': { cn: '请输入至少一项筛选条件', en: 'Please input one condition at least for filter', },
+ 'dataset.merge.validate.inputs': { cn: '请输入至少一项合并条件', en: 'Please input one condition at least for merge', },
+ 'dataset.add.internal.newkeywords.label': { en: 'Add following classes and related annotations:', cn: '添加以下类别及相应标注:'},
+ 'dataset.add.internal.ignore.all': { en: 'Ignore All', cn: '全部忽略'},
+ 'dataset.add.internal.ignorekeywords.label': { en: 'Ignore following classes and related annotations:', cn: '忽略以下类别及相应标注:'},
+ 'dataset.add.internal.add.all': { en: 'Add All', cn: '全部添加'},
+ "dataset.analysis.column.name": { cn: "数据集", en: "Dataset", },
+ "dataset.analysis.validator.dataset.count": { cn: "最多选择{count}个数据集", en: "Select {count} datasets at most", },
+ "dataset.analysis.column.version": { cn: "版本", en: "Version", },
+ "dataset.analysis.column.size": { cn: "数据集大小", en: "Dataset Size", },
+ "dataset.analysis.column.box_count": { cn: "标注框总数", en: "Annotations Count", },
+ "dataset.analysis.column.average_labels": { cn: "图片平均标注框数", en: "Average Annotations Per Asset", },
+ "dataset.analysis.column.overall": { cn: "已标注图片占比", en: "Labelled Assets Rate", },
+ "dataset.analysis.param.title": { cn: "选择", en: "Select", },
+ "dataset.analysis.btn.start": { cn: "开始分析", en: "Analysis", },
+ "dataset.analysis.btn.retry": { cn: "重新分析", en: "Retry", },
+ "dataset.analysis.title.asset_bytes": { cn: "图像大小分布", en: "Image Size Distribution", },
+ "dataset.analysis.title.asset_hw_ratio": { cn: "图像高宽比分布", en: "Image Aspect Ratio Distribution", },
+ "dataset.analysis.title.asset_area": { cn: "图像分辨率分布", en: "Image Resolution Distribution", },
+ "dataset.analysis.title.asset_quality": { cn: "图像质量分布", en: "Image Quality Distribution", },
+ "dataset.analysis.title.anno_area_ratio": { cn: "标注框分辨率分布", en: "Annotation Box Resolution Distribution", },
+ "dataset.analysis.title.keyword_ratio": { cn: "类别占比", en: "Classes Ratio", },
+ "dataset.analysis.bar.asset.tooltip": { cn: " 占比:{ratio} 数量:{amount} 张", en: " Ratio:{ratio} Amount:{amount}", },
+ "dataset.analysis.bar.anno.tooltip": { cn: " 占比:{ratio} 数量:{amount} 个", en: " Ratio:{ratio} Amount:{amount}", },
+ "dataset.train.all.train.target": { cn: "将训练集的所有类别作为训练目标", en: "All training dataset classes as training target", },
+ 'dataset.assets.keyword.selector.types.keywords': { en: 'Classes', cn: '类别' },
+ 'dataset.assets.keyword.selector.types.cks': { en: 'Asset Tag', cn: '数据标签' },
+ 'dataset.assets.keyword.selector.types.tags': { en: 'Box Tag', cn: '标注框标签' },
+ 'dataset.assets.keyword.selector.types.placeholder': { en: 'Please select filter classes', cn: '请选择筛选类别,可多选' },
+ 'dataset.assets.selector.gt.label': { en: 'Annotation Type:', cn: '标注类型:' },
+ 'dataset.assets.selector.evaluation.label': { en: 'Evaluation:', cn: '预测:' },
+ 'dataset.detail.infer.class': { en: 'Prediction Classes:', cn: '模型预测类别:' },
+
}
export default dataset
diff --git a/ymir/web/src/locales/modules/errors.ts b/ymir/web/src/locales/modules/errors.ts
index 76320adca5..3437957601 100644
--- a/ymir/web/src/locales/modules/errors.ts
+++ b/ymir/web/src/locales/modules/errors.ts
@@ -1,21 +1,23 @@
const errors = {
- 'error.timeout': { cn: '请求超时', en: 'Request Timeout', },
+ 'error.timeout': { cn: '网关请求超时,后端服务异常', en: 'Gateway Timeout, backend service expection', },
+ 'error.502': { cn: '网关异常,无法连接到后端服务', en: 'Bad Gateway, backend service disconnect', },
'error110101': { cn: '接口错误', en: 'API_ERROR: API Error', },
'error110102': { cn: '参数校验失败', en: 'Parammeters Validation Failed', },
'error110103': { cn: '未知错误', en: 'UNKNOWN_ERROR: Unkown Error', },
'error110104': { cn: 'token失效,请重新登录', en: 'INVALID_TOKEN: Invalid Token, Please try again', },
'error110105': { cn: '必填字段缺失', en: 'REQUIRED_FIELD_MISSING: ', },
- 'error110106': { cn: '服务器错误', en: 'CONTROLLER_ERROR: ', },
+ 'error110106': { cn: '服务器错误', en: 'CONTROLLER_ERROR', },
'error110107': { cn: '用户名或密码错误,请重试', en: 'Unmatch email or password, Please try again', },
'error110108': { cn: '下载失败', en: 'FAILED_TO_DOWNLOAD: failed to download file', },
'error110109': { cn: '配置不可用', en: 'INVALID_CONFIGURATION', },
'error110110': { cn: '用户权限不匹配', en: 'INVALID_SCOPE', },
'error110111': {
- cn: '无法隐藏受保护的资源,如当前迭代的产出、项目中的训练集、测试集、挖掘集或者正在进行的数据集/模型',
- en: 'Can not hide protected resource, such as result of current iteration, training, testing, and mining dataset related to project, or in-progress dataset/model',
+ cn: '无法隐藏受保护的资源,如当前迭代的产出、项目中的训练集、验证集、挖掘集或者正在进行的数据集/模型',
+ en: 'Can not hide protected resource, such as result of current iteration, training, validation, and mining dataset related to project, or in-progress dataset/model',
},
+ 'error110112': { cn: '系统升级导致token失效,需要重新登录', en: 'Invalid token by system upgrading, please login again', },
'error110201': { cn: '找不到该用户,请重试', en: 'USER_NOT_FOUND: User Not Found, retry or contact admin.', },
- 'error110202': { cn: '名称已重复,请选择新的名称注册', en: 'USER_DUPLICATED_NAME: Duplicated Username, try another name', },
+ 'error110202': { cn: '邮箱已注册,请选择新的邮箱注册', en: 'USER_DUPLICATED_NAME: Duplicated Email, try another one', },
'error110203': { cn: '用户未授权访问', en: 'USER_NOT_ACCESSIBLE: User is Unaccessable', },
'error110204': { cn: '用户未登录,请登录', en: 'USER_NOT_LOGGED_IN: User is not logged in, please log in', },
'error110205': { cn: '该操作需要管理员权限,用户不是管理员', en: 'USER_NOT_ADMIN: User is not admin but action need admin privelige', },
@@ -29,6 +31,12 @@ const errors = {
'error110403': { cn: '数据集未授权访问', en: 'Dataset inaccessable', },
'error110404': { cn: '数据集创建失败', en: 'DATASET_FAILED_TO_CREATE: failed to create dataset', },
'error110405': { cn: '数据集被系统保护不可删除', en: 'DATASET_FAILED_TO_DELETE: failed to delete dataset', },
+ 'error110407': {
+ cn: '数据集文件结构有误,请重新检查并修改后重试',
+ en: 'Dataset file structure unmatched, correct it and try again',
+ },
+ 'error110408': { cn: '导入数据集失败', en: 'Failed to import dataset', },
+ 'error110409': { cn: '压缩包无法解析', en: 'Zip file parse error', },
'error110501': { cn: '找不到该数据', en: 'Asset is not found', },
'error110601': { cn: '找不到该模型', en: 'Model is not found', },
'error110602': { cn: '重复的模型名称', en: 'Duplicated model name', },
@@ -42,10 +50,10 @@ const errors = {
'error110705': { cn: '任务状态过时', en: 'TASK_STATUS_OBSOLETE', },
'error110706': { cn: '更新任务状态失败', en: 'FAILED_TO_UPDATE_TASK_STATUS: failed to update task status', },
'error110801': { cn: '找不到对应的历史树', en: 'HISTORY NOT FOUND', },
- 'error111001': { cn: '重复的标签名称或别名', en: 'KEYWORD_DUPLICATED: duplicated keyword or aliases', },
+ 'error111001': { cn: '重复的类别名称或别名', en: 'KEYWORD_DUPLICATED: duplicated keyword or aliases', },
'error110901': { cn: '调用推理失败', en: 'INFERENCE_FAILED_TO_CALL: failed to call inference', },
- 'error110902': { cn: '推理镜像配置错误', en: 'INFERENCE_CONFIG_ERROR: inference image configure error', },
- 'error111101': { cn: '镜像名称重复', en: 'Duplicated docker image name', },
+ 'error110902': { cn: '推理镜像配置错误', en: 'Inference docker image configuration error', },
+ 'error111101': { cn: '镜像名称或地址重复', en: 'Duplicated docker image name/url', },
'error111102': { cn: '找不到镜像', en: 'Docker image is not found', },
'error111103': { cn: '共享镜像失败', en: 'Share docker image failed', },
'error111104': { cn: '此镜像关联其他镜像,请清除关联后再处理', en: 'Clean relationships of docker images before deleting it', },
@@ -56,11 +64,15 @@ const errors = {
'error111401': { cn: '找不到项目', en: 'Project Not Found', },
'error111402': { cn: '项目名称重复', en: 'Duplicated project name', },
'error111502': { cn: '数据集名称重复', en: 'Duplicated dataset name', },
+ 'error111602': { cn: '模型名称重复', en: 'Duplicated model name', },
'error111901': { cn: '隐藏和取消隐藏不能同时处理', en: 'Hide and Unhide cannot handle in same request', },
'error111904': { cn: '操作的数量为空', en: 'Operations is missing', },
'error110406': { cn: '不在同一个数据集的版本不能进行比对', en: 'Versions must be in the same datasets', },
'error111902': { cn: '调用CMD进行数据集比对失败', en: 'Evaluate error from CMD', },
'error111903': { cn: '完成数据集比对,但找不到相应的结果', en: 'Evaluate done, but can not find result', },
+ 'error111905': { cn: '推理结果缺乏真值或预测标注,诊断失败', en: 'Evaluation failed for no GT or prediction', },
+ 'error111906': { cn: '模型推理尚未完成,诊断失败', en: 'Evaluate failed for inference unfinished', },
+ 'error112103': { cn: '内部请求超时', en: 'Internal request timeout', },
'error130604': {
cn: '内部网络错误, 请检查应用程序系统配置',
en: 'HTTP_ERROR: internal network error, please check system configuration.',
@@ -78,19 +90,23 @@ const errors = {
'error150500': { cn: 'controller 内部错误', en: 'controller: internal_error', },
'error160001': { cn: 'CMD: 参数不可用', en: 'RC_CMD_INVALID_ARGS: invalid args', },
'error160002': { cn: 'CMD: 训练集为空', en: 'RC_CMD_EMPTY_TRAIN_SET: empty train set', },
- 'error160003': { cn: 'CMD: 测试集为空', en: 'RC_CMD_EMPTY_VAL_SET: empty test set', },
+ 'error160003': { cn: 'CMD: 验证集为空', en: 'RC_CMD_EMPTY_VAL_SET: empty validation set', },
'error160004': { cn: 'CMD: 容器错误', en: 'RC_CMD_CONTAINER_ERROR: image container error', },
'error160005': { cn: 'CMD: 未知类型', en: 'RC_CMD_UNKNOWN_TYPES: unkown type', },
'error160006': { cn: 'CMD: 分支或标签不可用', en: 'RC_CMD_INVALID_BRANCH_OR_TAG: invalid branch or tag', },
'error160007': { cn: 'CMD: 脏Repo,需要清理', en: 'RC_CMD_DIRTY_REPO: dirty repo.', },
'error160008': {
- cn: '合并错误,例如训练集和测试集有重合图片',
- en: 'Merge error, such as training dataset have the same assets in testing dataset.',
+ cn: '合并错误,例如训练集和验证集有重合图片',
+ en: 'Merge error, such as training dataset have the same assets in validation dataset.',
},
'error160009': { cn: 'CMD: mir repo不可用', en: 'RC_CMD_INVALID_MIR_REPO: invalid mir repo.', },
'error160010': { cn: 'CMD: 文件不可用', en: 'RC_CMD_INVALID_FILE: invalid file', },
'error160011': { cn: 'CMD: 无结果生成', en: 'RC_CMD_NO_RESULT: no result', },
+ 'error160015': { cn: '模型解析失败', en: 'Model parse failed', },
+ 'error160016': { cn: 'CMD: 读取meta.yaml文件失败', en: 'Reading meta.yaml file failed', },
'error169999': { cn: 'CMD: 未知错误', en: 'RC_CMD_ERROR_UNKNOWN: unkown error', },
+ 'error111705': {cn: '找不到迭代步骤', en: 'Iteration step is not found.', },
+ 'error111706': {cn: '迭代步骤已经完成', en: 'Iteration step is already finished.', },
}
export default errors
diff --git a/ymir/web/src/locales/modules/image.ts b/ymir/web/src/locales/modules/image.ts
index cb8ba51f91..9143676907 100644
--- a/ymir/web/src/locales/modules/image.ts
+++ b/ymir/web/src/locales/modules/image.ts
@@ -1,44 +1,44 @@
const image = {
- "images.title": { en: "Image List", cn: "镜像列表", },
+ "images.title": { en: "My Images", cn: "我的镜像", },
"image.title": { en: "Image Detail", cn: "镜像详情", },
"image.add.title": { en: "Create Image", cn: "添加镜像", },
"image.type.unkown": { en: "Pending", cn: "加载中", },
"image.type.mining": { en: "Mining", cn: "挖掘", },
"image.type.train": { en: "Training", cn: "训练", },
- "image.type.inference": { en: "Training", cn: "推理", },
+ "image.type.inference": { en: "Inference", cn: "推理", },
"image.state.pending": { en: "Pending", cn: "加载中", },
"image.state.done": { en: "Done", cn: "完成", },
"image.state.error": { en: "Error", cn: "失败", },
"image.query.name": { en: "Image Name", cn: "镜像名称", },
- "image.query.name.placeholder": { en: "Input Image Name", cn: "请输入名称", },
+ "image.query.name.placeholder": { en: "Input image name", cn: "请输入名称", },
"image.column.type": { en: "Image Type", cn: "镜像类型", },
"image.action.link": { en: "Relate Images", cn: "关联镜像", },
"image.action.share": { en: "Share Image", cn: "共享镜像", },
"image.action.edit": { en: "Edit Image", cn: "编辑镜像", },
"image.action.del": { en: "Delete Image", cn: "删除镜像", },
"image.action.detail": { en: "Image Detail", cn: "镜像详情", },
- "image.action.copy": { en: "Copy Image to List", cn: "复制镜像到镜像列表", },
- "image.tab.my.title": { en: "Images List", cn: "镜像列表", },
+ "image.action.copy": { en: "Copy Image", cn: "复制镜像到我的镜像", },
+ "image.tab.my.title": { en: "My Images", cn: "我的镜像", },
"image.tab.public.title": { en: "Public Images", cn: "公共镜像", },
"image.new.label": { en: "New Image", cn: "新增镜像", },
- "image.list.item.type": { en: "Image Type: ", cn: "镜像类型:", },
- "image.list.item.url": { en: "Image URL:", cn: "镜像地址:", },
- "image.list.item.desc": { en: "Image Description:", cn: "描述:", },
+ "image.list.item.type": { en: "Type: ", cn: "镜像类型:", },
+ "image.list.item.url": { en: "URL:", cn: "镜像地址:", },
+ "image.list.item.desc": { en: "Description:", cn: "描述:", },
"image.list.item.related": { en: "Related Images:", cn: "关联镜像:", },
"image.list.item.org": { en: "Organization", cn: "组织机构", },
"image.list.item.contributor": { en: "Contributor", cn: "贡献者", },
'image.add.success': { en: "Image Created!", cn: "添加镜像成功", },
'image.update.success': { en: "Image Updated!", cn: "编辑镜像成功", },
'image.add.form.url.invalid': { en: "Invalid Image, check the input and try again", cn: "镜像名称不合法,请输入dockerhub的镜像名称", },
- 'breadcrumbs.image.add': { en: "Create Image", cn: "创建镜像", },
- 'image.add.form.url': { en: "Image", cn: "镜像", },
- 'image.add.form.url.required': { en: "Please input Docker-Hub image name", cn: "请输入镜像URL(docker image 名称,可带tag)", },
+ 'breadcrumbs.image.add': { en: "New Image", cn: "新增镜像", },
+ 'image.add.form.url': { en: "Docker Image URL", cn: "镜像地址", },
+ 'image.add.form.url.required': { en: "Please input docker image url, from Docker Hub etc.", cn: "请输入镜像URL(如 Docker Hub image 名称,可带tag)", },
'image.add.form.url.placeholder': { en: "E.G. ubuntu:18:04, default latest tag if not given",
cn: "例:ubuntu:18.04 。Docker Hub上的镜像名称,可指定Tag,不指定则为latest", },
'image.add.form.name': { en: "Image Name", cn: "镜像名称", },
'image.add.form.name.placeholder': { en: "Please input your image name", cn: "请输入镜像名称", },
- 'image.add.form.desc': { en: "Image Desc.", cn: "镜像描述", },
- 'image.add.submit': { en: "Create Image", cn: "创建镜像", },
+ 'image.add.form.desc': { en: "Description", cn: "描述", },
+ 'image.add.submit': { en: "New Image", cn: "新增镜像", },
'image.update.submit': { en: "Update Image", cn: "编辑镜像", },
'image.list.total': { en: "Total {total} Image", cn: "总共 {total} 个镜像", },
'image.del.confirm.content': { en: "Are you sure to remove this image:{name}?", cn: "确认要删除镜像:{name}?", },
@@ -50,7 +50,7 @@ const image = {
'image.link.name': { en: "Image Name", cn: "镜像名称", },
'image.links.title': { en: "Relate Image", cn: "关联镜像", },
'image.share.title': { en: "Share Image {name}", cn: "共享镜像 {name}", },
- 'image.links.placeholder': { en: "Please select mining images for train image", cn: "请选择关联训练镜像的挖掘镜像", },
+ 'image.links.placeholder': { en: "Please select mining images for relating train image", cn: "请选择关联训练镜像的挖掘镜像", },
'image.detail.title': { en: "Image Detail", cn: "镜像详情", },
'image.detail.label.name': { en: "Name", cn: "镜像名称", },
'image.detail.label.type': { en: "Type", cn: "镜像类型", },
@@ -62,6 +62,8 @@ const image = {
'image.detail.relate': { en: "Relate", cn: "去关联", },
'image.select.opt.related': { en: "Related Images", cn: "关联镜像(推荐)", },
'image.select.opt.normal': { en: "List", cn: "列表", },
+ 'image.livecode.label.remote': { en: "Remote", cn: "远端", },
+ 'image.livecode.label.local': { en: "Local", cn: "本地", },
}
export default image
diff --git a/ymir/web/src/locales/modules/keyword.ts b/ymir/web/src/locales/modules/keyword.ts
index 671313e572..a821f034c1 100644
--- a/ymir/web/src/locales/modules/keyword.ts
+++ b/ymir/web/src/locales/modules/keyword.ts
@@ -1,26 +1,26 @@
const keyword = {
- "keyword.column.name": { en: "Keyword Name", cn: "标签名称", },
- "keyword.column.alias": { en: "Keyword Alias", cn: "别名", },
+ "keyword.column.name": { en: "Class Name", cn: "类别名称", },
+ "keyword.column.alias": { en: "Class Alias", cn: "别名", },
"keyword.column.action": { en: "Actions", cn: "操作", },
- "keyword.query.name.placeholder": { en: "Please input keyword name", cn: "请输入标签名称", },
- "keyword.pager.total.label": { en: "Total {total} keywords", cn: "共 {total} 个标签", },
- "keyword.add.label": { en: "Add Keyword", cn: "添加标签", },
- "keyword.empty.label": { en: "Add Keywords", cn: "添加标签", },
+ "keyword.query.name.placeholder": { en: "Please input class name", cn: "请输入类别名称", },
+ "keyword.pager.total.label": { en: "Total {total} classes", cn: "共 {total} 个类别", },
+ "keyword.add.label": { en: "Add Class", cn: "添加类别", },
+ "keyword.empty.label": { en: "Add Classes", cn: "添加类别", },
"keyword.empty.btn.label": { en: "Add", cn: "添加", },
- "keyword.add.name.label": { en: "Keyword", cn: "标签", },
+ "keyword.add.name.label": { en: "Class", cn: "类别", },
"keyword.add.alias.label": { en: "Alias", cn: "别名", },
- "keyword.add.alias.placeholder": { en: "Please input alias of keyword, multiple slipt by comma ','", cn: "请输入别名,多个用英文逗号','间隔", },
- 'keyword.add.success': {en: "Keywords added success", cn: "标签添加成功", },
- 'keyword.add.failure': {en: "Keywords added failure", cn: "标签添加失败", },
- 'keyword.name.repeat': {en: "Repeated keyword or alias", cn: "重复的标签或别名", },
- 'keyword.add.name.placeholder': {en: "Please input keyword name", cn: "请输入标签名称", },
+ "keyword.add.alias.placeholder": { en: "Please input alias of class, multiple slipt by comma ','", cn: "请输入别名,多个用英文逗号','间隔", },
+ 'keyword.add.success': {en: "Classes added success", cn: "类别添加成功", },
+ 'keyword.add.failure': {en: "Classes added failure", cn: "类别添加失败", },
+ 'keyword.name.repeat': {en: "Repeated class or alias", cn: "重复的类别或别名", },
+ 'keyword.add.name.placeholder': {en: "Please input class name", cn: "请输入类别名称", },
'keyword.add.name.validchar': {en: "Name or Alias exceeds 32 characters limit or included ilegal char or comma", cn: "字符超过32、字符不合法或包含英文逗号", },
- 'keyword.add.name.required': {en: "Keyword name required", cn: "标签名称不能为空", },
- 'keyword.multiadd.success': {en: "Keywords added", cn: "批量添加标签成功", },
- 'keyword.multiadd.invalid': {en: "Invalid format of keywords", cn: "标签格式不合法", },
- 'keyword.multiadd.title': {en: "Add Multiple Keywords", cn: "批量添加标签", },
- 'keyword.multiadd.kws.label': {en: "Keywords", cn: "标签", },
- 'keyword.multiadd.label': {en: "Add Keywords", cn: "批量添加标签", },
+ 'keyword.add.name.required': {en: "Class name required", cn: "类别名称不能为空", },
+ 'keyword.multiadd.success': {en: "Classes added", cn: "批量添加类别成功", },
+ 'keyword.multiadd.invalid': {en: "Invalid format of classes", cn: "类别格式不合法", },
+ 'keyword.multiadd.title': {en: "Add Multiple Classes", cn: "批量添加类别", },
+ 'keyword.multiadd.kws.label': {en: "Classes", cn: "类别", },
+ 'keyword.multiadd.label': {en: "Add Classes", cn: "批量添加类别", },
'keyword.column.update_time': {en: "Update Time", cn: "更新时间", },
'keyword.column.create_time': {en: "Create Time", cn: "创建时间", },
'keyword.multiadd.kws.placeholder': {
@@ -29,11 +29,12 @@ const keyword = {
cat:kitty
cow:bull,cattle`,
cn:
-`可批量添加标签,多个标签使用换行分隔;
-如有别名,可在标签名称后面用冒号分隔。
+`可批量添加类别,多个类别使用换行分隔;
+如有别名,可在类别名称后面用冒号分隔。
多个别名用英文逗号分隔。例:
cat:kitty
cow:bull,cattle`, },
+ 'keyword.ck.label': {en: "Asset Tag", cn: "数据标签", },
}
export default keyword
diff --git a/ymir/web/src/locales/modules/model.ts b/ymir/web/src/locales/modules/model.ts
index ba5ceb8b94..ddad29320b 100644
--- a/ymir/web/src/locales/modules/model.ts
+++ b/ymir/web/src/locales/modules/model.ts
@@ -1,13 +1,18 @@
const model = {
"model.detail.title": { en: "Model Detail", cn: "模型详情", },
- "model.column.name": { en: "Model Name", cn: "模型名称", },
+ "model.diagnose": { en: "Model Diagnose", cn: "模型诊断", },
+ "model.management": { en: "Model Management", cn: "模型管理", },
+ "model.list": { en: "Model List", cn: "模型列表", },
+ "model.column.name": { en: "Version", cn: "版本", },
"model.column.source": { en: "Source", cn: "来源", },
- "model.column.target": { en: "Train Classes", cn: "训练目标", },
+ "model.column.target": { en: "Target", cn: "训练目标", },
"model.column.map": { en: "mAP", cn: "精度均值(mAP)", },
+ "model.column.stage": { en: "Intermediate", cn: "中间模型", },
"model.column.create_time": { en: "Create Time", cn: "创建时间", },
"model.column.action": { en: "Actions", cn: "操作", },
"model.action.download": { en: "Download", cn: "下载", },
"model.action.verify": { en: "Verify", cn: "验证", },
+ "model.action.publish": { en: "Publish", cn: "发布", },
"model.empty.label": { en: "Train A Model", cn: "训练出一个模型", },
"model.empty.btn.label": { en: "Import Model", cn: '导入模型', },
"model.import.label": { en: "Import Model", cn: "导入模型", },
@@ -19,12 +24,13 @@ const model = {
"model.pager.total.label": { en: "Total {total} items", cn: "共 {total} 项", },
'model.detail.label.name': { en: 'Model Name', cn: '模型名称', },
'model.detail.label.map': { en: 'mAP', cn: 'mAP值', },
+ 'model.detail.label.stage': { en: 'Intermediates', cn: '中间模型', },
'model.detail.label.source': { en: 'Source', cn: '模型来源', },
'model.detail.label.image': { en: 'Train Image', cn: '训练镜像', },
'model.detail.label.training_dataset': { en: 'Training Dataset', cn: '训练集', },
- 'model.detail.label.test_dataset': { en: 'Test Dataset', cn: '测试集', },
+ 'model.detail.label.test_dataset': { en: 'Validation Dataset', cn: '验证集', },
'model.detail.label.train_type': { en: 'Train Type', cn: '训练类型', },
- 'model.detail.label.train_goal': { en: 'Train Classes', cn: '训练目标', },
+ 'model.detail.label.train_goal': { en: 'Target', cn: '训练目标', },
'model.detail.label.framework': { en: 'Network', cn: '算法框架', },
'model.detail.label.backbone': { en: 'Backbone', cn: '骨干网络结构', },
'model.add.types.copy': { en: 'Share', cn: '复制模型', },
@@ -34,9 +40,10 @@ const model = {
'model.add.form.name': { en: 'Name', cn: '名称', },
'model.add.form.name.placeholder': { en: 'Model Name', cn: '请输入模型名称', },
'model.add.form.type': { en: 'Import Type', cn: '导入类型', },
- 'model.add.form.project': { en: 'Original Model', cn: '待复制模型', },
+ 'model.add.form.project': { en: 'Select Model', cn: '选择模型', },
'model.add.form.upload.btn': { en: 'Upload', cn: '上传文件', },
- 'model.add.form.url': { en: 'Url', cn: '网络地址', },
+ 'model.add.form.url': { en: 'Url', cn: 'URL地址', },
+ 'model.add.form.url.help': { en: 'E.g. https://github.com/yolo5_model/cat', cn: '示例:https://github.com/yolo5_model/cat', },
'model.add.form.url.tip': { en: 'Please input valid url for model', cn: '请输入正确的网络地址,指向模型文件', },
'model.add.form.url.placeholder': { en: 'Please input model url from internet', cn: '请输入模型文件的网络地址', },
'model.file.required': { en: 'Please upload model', cn: '请上传模型', },
@@ -49,6 +56,42 @@ const model = {
"model.verify.model.param.fold": { cn: '点击收起', en: 'Fold', },
"model.verify.model.param.unfold": { cn: '点击展开', en: 'Unfold', },
'model.verify.upload.tip': { cn: '模型验证需要较长时间,请耐心等待', en: 'Verification need more time, be patient...' },
+ "model.diagnose.tab.metrics": { cn: '衡量指标', en: 'Metrics', },
+ "model.diagnose.tab.training": { cn: '训练过程', en: 'Training Fitting', },
+ "model.diagnose.tab.visualization": { cn: '图像可视化', en: 'Image Visualization', },
+ "model.diagnose.form.model": { cn: '诊断模型', en: 'Diagnosing Models', },
+ 'model.diagnose.form.testset': { cn: '测试集', en: 'Testing Datasets', },
+ 'model.diagnose.form.confidence': { cn: '置信度', en: 'Confidence', },
+ 'model.diagnose.form.iou': { cn: '请输入mAP计算方式', en: 'mAP Calculation', },
+ 'model.diagnose.form.iou.everage': { cn: '插值计算', en: 'Average IoU', },
+ 'model.diagnose.form.iou.single': { cn: '单点计算', en: 'Single Point', },
+ 'model.diagnose.restart': { cn: '重新比对', en: 'Compare Again', },
+ "model.action.diagnose.training.retry": { cn: '重新诊断', en: 'Retry', },
+ "model.diagnose.label.model": { cn: "模型", en: "Models", },
+ "model.diagnose.label.testing_dataset": { cn: "测试集", en: "Testing Datasets", },
+ "model.diagnose.label.config": { cn: "推理配置", en: "Infer Configs", },
+ "model.diagnose.stage.label": { cn: "设置中间模型", en: "Set Recommended Intermediate", },
+ "model.diagnose.metrics.precision.label": {cn: '精确率', en: 'Precision', },
+ "model.diagnose.metrics.precision.average.label": {cn: '平均召回率', en: 'Average Recall', },
+ "model.diagnose.metrics.precision.target.label": {cn: '{label}召回率 | 置信度', en: '{label} Recall | Confidence', },
+ "model.diagnose.metrics.recall.label": {cn: '召回率', en: 'Recall', },
+ "model.diagnose.metrics.recall.average.label": {cn: '平均精确率', en: 'Average Precision', },
+ "model.diagnose.metrics.recall.target.label": {cn: '{label}精确率 | 置信度', en: '{label} Precision | Confidence', },
+ "model.diagnose.metrics.confidence.average.label": {cn: '平均置信度', en: 'Average Confidence', },
+ "model.diagnose.medtric.tabs.map": {cn: 'mAP', en: 'mAP', },
+ "model.diagnose.medtric.tabs.curve": {cn: 'PR曲线', en: 'PR Curve', },
+ "model.diagnose.medtric.tabs.rp": {cn: '指定召回率', en: 'Recall', },
+ "model.diagnose.medtric.tabs.pr": {cn: '指定精确率', en: 'Precision', },
+ 'model.diagnose.metrics.ck.placeholder': {en: "Please select a asset tag", cn: "请选择一类数据标签", },
+ 'model.diagnose.metrics.keyword.placeholder': {en: "Please select classes", cn: "请选择类别", },
+ 'model.diagnose.metrics.view.label': {en: "View", cn: "视图", },
+ 'model.diagnose.metrics.dimension.label': {en: "Dimension:", cn: "维度:", },
+ "model.diagnose.metrics.btn.start": { cn: "开始诊断", en: "Diagnose", },
+ "model.diagnose.metrics.btn.retry": { cn: "重新诊断", en: "Retry", },
+ 'model.diagnose.v.tasks.require': {en: "Please select infered testing dataset and config", cn: "请选择推理过的测试集及配置", },
+ 'model.diagnose.metrics.x.dataset': {en: "Testing Dataset", cn: "测试集", },
+ 'model.diagnose.metrics.x.keyword': {en: "Class", cn: "类别", },
+ 'model.list.batch.invalid': {en: "Please select valid model to batch", cn: "请选择有效的模型进行批量操作", },
}
export default model
diff --git a/ymir/web/src/locales/modules/portal.ts b/ymir/web/src/locales/modules/portal.ts
index 6cba606d39..18a35bb668 100644
--- a/ymir/web/src/locales/modules/portal.ts
+++ b/ymir/web/src/locales/modules/portal.ts
@@ -5,8 +5,8 @@ const portal = {
'portal.dataset.origin.title': { cn: '公共数据集', en: 'Public Dataset', },
'portal.project.my.title': { cn: '我的项目', en: 'My Project', },
'portal.dataset.asset.count': { cn: '图片数', en: 'Assets', },
- 'portal.dataset.keyword.count': { cn: '标签数', en: 'Keywords', },
- 'portal.dataset.keyword': { cn: '标签', en: 'Keywords', },
+ 'portal.dataset.keyword.count': { cn: '类别数', en: 'Classes', },
+ 'portal.dataset.keyword': { cn: '类别', en: 'Classes', },
'portal.action.new.project': { cn: '新建一个项目', en: 'Create Project', },
}
diff --git a/ymir/web/src/locales/modules/project.ts b/ymir/web/src/locales/modules/project.ts
index 763922b139..57014daae3 100644
--- a/ymir/web/src/locales/modules/project.ts
+++ b/ymir/web/src/locales/modules/project.ts
@@ -1,8 +1,9 @@
const project = {
- "projects.title": { en: "Project List", cn: "项目列表", },
+ "projects.title": { en: "Projects", cn: "项目管理", },
"project.title": { en: "Project Detail", cn: "项目详情", },
+ "project.summary": { en: "Project Summary", cn: "项目概览", },
"project.add.title": { en: "Create Project", cn: "创建项目", },
- "project.settings.title": { en: "Project Settings", cn: "设置项目", },
+ "project.settings.title": { en: "Project Settings", cn: "项目设置", },
"project.iterations.title": { en: "Project Iterations", cn: "项目迭代", },
"project.hidden.title": { en: "Hidden List", cn: "隐藏列表", },
"project.action.edit": { en: "Edit Project", cn: "编辑项目", },
@@ -15,7 +16,7 @@ const project = {
"project.target.map": { en: "Target mAP", cn: "目标mAP", },
"project.iteration.current": { en: "Iteration Stage", cn: "迭代进度", },
"project.train_set": { en: "Training Set", cn: "训练集", },
- "project.test_set": { en: "Test Set", cn: "测试集", },
+ "project.test_set": { en: "Validation Set", cn: "验证集", },
"project.mining_set": { en: "Mining Set", cn: "挖掘集", },
"project.iteration.number": { en: "Iteration Number", cn: "迭代轮次", },
"project.content.desc": { en: "Description", cn: "描述", },
@@ -34,11 +35,21 @@ const project = {
'project.add.form.keyword.required': { en: 'Training classes is required', cn: '训练目标为必选项', },
'project.add.form.keyword.placeholder': {
en: 'Please select training classes, or input new classes, separated by comma',
- cn: '请输入训练目标,可选择已有的或输入新标签,英文逗号分隔',
+ cn: '请输入训练目标,可选择已有的或输入新类别,英文逗号分隔',
},
'project.add.form.keyword.tip': {
- en: 'The target tags used for training need to match the categories in the user\'s tag list. If the input tag does not exist in the current user\'s tag list, it will be prompted to add it to the list when creating the project.',
- cn: '用于训练的目标标签,需要和用户的标签列表里的类别一致,如果输入的标签在当前用户的标签列表中不存在,则会在创建项目时提示将其加入列表。',
+ en: 'The target classes need to match user\'s classes. If the input tag does not exist in user\'s classes, it will be prompted to add it to the list when creating the project.',
+ cn: '用于训练的目标类别,需要和用户的类别一致,如果输入的类别在当前用户的类别列表中不存在,则会在创建项目时提示将其加入列表。',
+ },
+ 'project.add.form.enableIteration': { en: 'Open A Semi-automated Iterative Process', cn: '开启半自动化迭代流程', },
+ 'project.add.form.enableIteration.tip': {
+ en: 'To assist users to achieve iterative optimization of the model through a fixed process, recommended for novice users.',
+ cn: '通过固定流程辅助用户实现模型的迭代优化,推荐新手用户选择。',
+ },
+ 'project.add.form.testingset.required': { en: 'Testing dataset is required', cn: '测试集为必选项', },
+ 'project.add.form.testingset.tip': {
+ en: 'Used to test the effect of the model in various data sets, note: the test set cannot be used for training.',
+ cn: '用于测试模型在各类数据集的效果指标,注:测试集不可用于训练。',
},
'project.add.form.target': { en: 'Project Target', cn: '目标设置', },
'project.add.form.target.map': { en: 'mAP', cn: 'mAP', },
@@ -58,7 +69,7 @@ const project = {
'project.iteration.stage.label': { en: 'Label', cn: '数据标注', },
'project.iteration.stage.merge': { en: 'Merge', cn: '更新训练集', },
'project.iteration.stage.training': { en: 'Training', cn: '模型训练', },
- 'project.iteration.stage.next': { en: 'Next Iteration', cn: '开启下一轮迭代', },
+ 'project.iteration.stage.next': { en: 'Next Iteration', cn: '下一轮迭代', },
'project.iteration.stage.datasets.react': { en: 'Re-process', cn: '重新设置数据', },
'project.iteration.stage.model.react': { en: 'Re-process', cn: '重新选择模型', },
'project.iteration.stage.ready.react': { en: 'Re-process', cn: '重新处理', },
@@ -66,13 +77,22 @@ const project = {
'project.iteration.stage.label.react': { en: 'Re-process', cn: '重新标注', },
'project.iteration.stage.merge.react': { en: 'Re-process', cn: '重新更新', },
'project.iteration.stage.training.react': { en: 'Re-process', cn: '重新训练', },
+ 'project.prepare.trainset': { en: 'Candidate Training Set', cn: '设置训练集', },
+ 'project.prepare.validationset': { en: 'Validation Set', cn: '设置验证集', },
+ 'project.prepare.miningset': { en: 'Mining Set', cn: '设置挖掘集', },
+ 'project.prepare.model': { en: 'Initial Model', cn: '设置初始模型', },
+ 'project.prepare.start': { en: 'Re-process', cn: '使用迭代功能提升模型效果', },
+ 'project.stage.state.done': { en: 'Unfinished', cn: '已完成', },
+ 'project.stage.state.waiting': { en: 'Unfinished', cn: '待选择', },
'project.stage.state.pending': { en: 'Unfinished', cn: '未完成', },
'project.stage.state.pending.current': { en: 'Pending', cn: '待完成', },
'project.iteration.settings.title': { en: 'Iterations Settings', cn: '迭代设置', },
'project.add.form.training.set': { en: 'Training Dataset', cn: '训练集', },
- 'project.add.form.test.set': { en: 'Test Dataset', cn: '测试集', },
+ 'project.add.form.training.set.version': { en: 'Version', cn: '训练集版本', },
+ 'project.add.form.test.set': { en: 'Validation Dataset', cn: '验证集', },
+ 'project.add.form.testing.set': { en: 'Testing Dataset', cn: '测试集', },
'project.add.form.mining.set': { en: 'Mining Dataset', cn: '挖掘集', },
- 'project.add.form.mining.strategy': { en: 'Mining Strategy', cn: '挖掘策略', },
+ 'project.add.form.mining.strategy': { en: 'Mining Strategy', cn: '选择挖掘策略', },
'project.add.form.mining.chunksize': { en: 'Chunk Size', cn: '每块数据量大小', },
'project.mining.strategy.0': { en: 'Chunk Mining', cn: '分块挖掘(在迭代中对挖掘集进行分块处理)', },
'project.mining.strategy.1': { en: 'Dedup Mining', cn: '去重挖掘(在迭代中会将之前迭代的挖掘数据排除出去)', },
@@ -92,13 +112,17 @@ const project = {
'iteration.column.label': { en: 'Labelling Result', cn: '标注结果', },
'iteration.column.merging': { en: 'Training Dataset', cn: '训练数据', },
'iteration.column.training': { en: 'Model', cn: '训练结果|mAP', },
- 'iteration.column.test': { en: 'Testing Dataset', cn: '测试集', },
+ 'iteration.column.test': { en: 'Validation Dataset', cn: '验证集', },
'project.detail.desc': { en: 'Description', cn: '描述', },
+ 'project.detail.datavolume': { en: 'Data Volume', cn: '数据量', },
+ 'project.detail.runningtasks': { en: 'Running Tasks', cn: '运行中任务', },
+ 'project.detail.totaltasks': { en: 'Total Tasks', cn: '总任务', },
'project.target.dataset': { en: 'Training Dataset\'s Assets', cn: '目标训练集大小', },
'project.initmodel.success.msg': { en: 'Initial model prepared', cn: '设置初始模型成功', },
'project.tag.train': { en: 'Training Dataset', cn: '训练集', },
- 'project.tag.test': { en: 'Testing Dataset {version}', cn: '测试集 {version}', },
+ 'project.tag.test': { en: 'Validation Dataset {version}', cn: '验证集 {version}', },
'project.tag.mining': { en: 'Mining Dataset {version}', cn: '挖掘集 {version}', },
+ 'project.tag.testing': { en: 'Testing Dataset', cn: '测试集', },
'project.tag.model': { en: 'Initial Model {version}', cn: '初始模型 {version}', },
'iteration.tag.round': { en: 'Round {round}', cn: '迭代{round}', },
'project.del.confirm.content': {
@@ -106,22 +130,47 @@ const project = {
cn: '删除项目会将项目中的所有资源(数据集、模型)删除,请谨慎操作!',
},
'project.add.confirm.title': {
- en: 'Whether this new keywords will add to your KEYWORD LIST?',
- cn: '标签管理列表未查询到下列标签,是否要添加至标签列表',
+ en: 'Whether this new keywords will add to your Classes?',
+ cn: '类别管理列表未查询到下列类别,是否要添加至类别列表',
},
- 'project.add.confirm.ok': { en: 'Add Keywords and Create Project', cn: '添加标签并创建项目', },
+ 'project.add.confirm.ok': { en: 'Add Classes and Create Project', cn: '添加类别并创建项目', },
'project.add.confirm.cancel': { en: 'Cancel Create Project', cn: '取消创建项目', },
'project.empty.label': {
en: 'You can manage datasets, train models, and create data iterations.',
cn: '在项目中可以管理数据集、训练模型、迭代数据',
},
- 'project.new.example.label': { en: 'Create Example Project', cn: '创建示例项目', },
+ 'project.new.example.label': { en: 'Add Example Project', cn: '添加示例项目', },
+ 'project.example': { en: 'Example Project', cn: '示例项目', },
'project.keywords.invalid': { en: 'Invalid training keywords', cn: '训练目标不合法', },
'project.workspace.status.dirty': {
- en: 'Project is {dirtyLabel}, can not train, please check again.',
- cn: '项目当前状态为{dirtyLabel},无法创建训练任务,请重新检查状态',
+ en: 'Project current workspace is {dirtyLabel}, training is disabled, please check again. if it is persistence, contact to administrator.',
+ cn: '项目当前的工作空间状态为{dirtyLabel},无法创建训练任务,请重新检查状态。若状态持续异常,请联系管理员。',
+ },
+ 'project.workspace.status.clean': { en: 'Project current workspace is {cleanLabel}, Training is enabled.', cn: '项目当前的工作空间状态为{cleanLabel}, 可以正常创建训练任务', },
+ 'project.testing.dataset.label': { en: 'Project Testing Dataset', cn: '项目测试集', },
+ 'project.iteration.entrance.status': {
+ en: 'You have processing iterations, current step is {stateLabel}',
+ cn: '您有正在进行中的迭代,当前进度为{stateLabel}',
+ },
+ 'project.iteration.entrance.empty.info': {
+ en: 'iterations supplied all-processing model Optimization production Recommanded for valueable data mining, training dataset expended, and comparison between datasets or models',
+ cn: '由系统辅助您进行全流程模型优化生产,包括有效数据的挖掘、训练集的扩充以及数据模型版本间的比对,推荐您使用',
},
- 'project.workspace.status.clean': { en: 'Project is {cleanLabel}.', cn: '项目当前状态为{cleanLabel}', },
+ 'project.iteration.entrance.empty.label': { en: 'No Iterations', cn: '暂无迭代', },
+ 'project.iteration.entrance.empty.btn': { en: 'Processing Models Training', cn: '系统辅助式模型生产', },
+ 'project.iteration.entrance.btn': { en: 'Enter Iterations', cn: '进入迭代', },
+ 'project.iteration.tabs.current': { en: 'Current Iteration', cn: '当前迭代', },
+ 'project.iteration.tabs.list': { en: 'Iteration Records', cn: '迭代历史', },
+ 'project.iteration.mining.all.processed': { en: 'Iteration Records', cn: '已挖掘数据占比', },
+ 'project.iteration.mining.keywords.processed': { en: 'Iteration Records', cn: '已挖掘数据中正负样本占比', },
+ "project.prepare.trainset.upload": { en: "Add Training Dataset", cn: "添加训练集", },
+ "project.prepare.validationset.upload": { en: "Add Testing Dataset", cn: "添加测试集", },
+ "project.prepare.miningset.upload": { en: "Add Mining Dataset", cn: "添加挖掘集", },
+ "project.iteration.detail.settings.title": { en: "Iteration Settings", cn: "迭代设置", },
+ "project.iteration.detail.intermediations.title": { en: "Intermediation", cn: "中间数据", },
+ "project.iteration.detail.models.title": { en: "Models", cn: "结果模型", },
+ "iteration.fold": { en: "Fold", cn: "收起操作项", },
+ "iteration.unfold": { en: "Unfold", cn: "展开操作项", },
}
export default project
diff --git a/ymir/web/src/locales/modules/routeTitle.ts b/ymir/web/src/locales/modules/routeTitle.ts
index eb2de66b52..dd4f59b47e 100644
--- a/ymir/web/src/locales/modules/routeTitle.ts
+++ b/ymir/web/src/locales/modules/routeTitle.ts
@@ -8,24 +8,28 @@ const routeTitle = {
"reset_pwd.title": { cn: `${SysName}-重置密码`, en: `${SysName} - Reset Password`, },
"datasets.title": { cn: `${SysName}-数据集列表`, en: `${SysName} - Dataset List`, },
"dataset.title": { cn: `${SysName}-数据集详情`, en: `${SysName} - Dataset Detail`, },
- "dataset.add.title": { cn: `${SysName}-导入数据集`, en: `${SysName} - Dataset Import`, },
+ "dataset.add.title": { cn: `${SysName}-添加数据集`, en: `${SysName} - Add Dataset`, },
+ "dataset.analysis.title": { cn: `${SysName}-数据集分析`, en: `${SysName} - Dataset Analysis`, },
"dataset.copy.title": { cn: `${SysName}-数据集复制`, en: `${SysName} - Dataset Copy`, },
- "dataset.compare.title": { cn: `${SysName}-数据集比对`, en: `${SysName} - Dataset Comparison`, },
"assets.title": { cn: `${SysName}-数据列表`, en: `${SysName} - Asset List`, },
"asset.title": { cn: `${SysName}-数据详情`, en: `${SysName} - Asset Detail`, },
"models.title": { cn: `${SysName}-模型列表`, en: `${SysName} - Model List`, },
"model.title": { cn: `${SysName}-模型详情`, en: `${SysName} - Model Detail`, },
+ "model.diagnose.title": { cn: `${SysName}-模型诊断`, en: `${SysName} - Model Diagnose`, },
"model.verify.title": { cn: `${SysName}-模型验证`, en: `${SysName} - Model Verify`, },
- "task.fusion.title": { cn: `${SysName}-数据预处理`, en: `${SysName} - Data Pretreatment`, },
+ "task.fusion.title": { cn: `${SysName}-挖掘数据准备`, en: `${SysName} - Data Pretreatment`, },
+ "task.merge.title": { cn: `${SysName}-数据合并`, en: `${SysName} - Data Merge`, },
+ "task.filter.title": { cn: `${SysName}-数据筛选`, en: `${SysName} - Data Filter`, },
"task.train.title": { cn: `${SysName}-模型训练`, en: `${SysName} - Model Training`, },
"task.mining.title": { cn: `${SysName}-数据挖掘`, en: `${SysName} - Dataset Mining`, },
"task.inference.title": { cn: `${SysName}-数据推理`, en: `${SysName} - Dataset Inference`, },
"task.label.title": { cn: `${SysName}-数据标注`, en: `${SysName} - Dataset Labeling`, },
"history.title": { cn: `${SysName}-历史树`, en: `${SysName} - History Tree`, },
- "keywords.title": { cn: `${SysName}-标签管理`, en: `${SysName} - Keywords`, },
+ "keywords.title": { cn: `${SysName}-类别管理`, en: `${SysName} - Classes`, },
"projects.title": { cn: `${SysName}-项目管理`, en: `${SysName} - Project`, },
"project.title": { cn: `${SysName}-项目详情`, en: `${SysName} - Project Detail`, },
- "project.add.title": { cn: `${SysName}-设置项目`, en: `${SysName} - Project Settings`, },
+ "project.add.title": { cn: `${SysName}-项目设置`, en: `${SysName} - Project Settings`, },
+ "project.iteration.add.title": { cn: `${SysName}-迭代设置`, en: `${SysName} - Iteration Settings`, },
"project.iteration.title": { cn: `${SysName}-迭代详情`, en: `${SysName} - Project Iteration`, },
}
diff --git a/ymir/web/src/locales/modules/signup.ts b/ymir/web/src/locales/modules/signup.ts
index 5d1fae7a0f..96c49ed471 100644
--- a/ymir/web/src/locales/modules/signup.ts
+++ b/ymir/web/src/locales/modules/signup.ts
@@ -15,13 +15,17 @@ const signup = {
"signup.pwd.length.msg": { cn: "密码长度在{min}到{max}之间", en: "Password length is between {min} and {max}", },
"signup.pwd.repeat.required.msg": { cn: "请确认密码", en: "Please confirm your password", },
"signup.title.page": { cn: "用户注册", en: "Sign Up", },
- "signup.login.tip": { cn: "已有账号?", en: "Have account?", },
+ "signup.login.tip": { cn: "已有账号?", en: "Have account? ", },
"signup.login.label": { cn: "去登录>", en: "Login>", },
"signup.email.placeholder": { cn: "请输入邮箱", en: "Please input your email", },
"signup.username.placeholder": { cn: "请输入用户名", en: "Please input your username", },
"signup.phone.placeholder": { cn: "请输入手机号码", en: "Please input your phone", },
"signup.pwd.placeholder": { cn: "请输入密码", en: "Please input your password", },
"signup.repwd.placeholder": { cn: "请再次输入密码", en: "Please input your password again", },
+ "signup.org": { cn: "所属机构", en: "Organization", },
+ "signup.org.placeholder": { cn: "请输入您所在的机构/学校名称", en: "Please input school/org. you work for", },
+ "signup.scene": { cn: "应用场景", en: "Usage Scene", },
+ "signup.scene.placeholder": { cn: "请输入你的具体应用场景描述", en: "Please describe usage scene", },
}
export default signup
diff --git a/ymir/web/src/locales/modules/task.ts b/ymir/web/src/locales/modules/task.ts
index 30b983077b..e57e837940 100644
--- a/ymir/web/src/locales/modules/task.ts
+++ b/ymir/web/src/locales/modules/task.ts
@@ -3,8 +3,10 @@ const task = {
"task.type.mining": { cn: "挖掘", en: "Mining", },
"task.type.label": { cn: "标注", en: "Label", },
"task.type.fusion": { cn: "预处理", en: "Pretreat", },
+ 'task.type.merge': { cn: '合并', en: 'Merge', },
+ 'task.type.filter': { cn: '筛选', en: 'Filter', },
"task.type.inference": { cn: "推理", en: "Inference", },
- "task.type.import": { cn: "数据集导入", en: "Dataset Import", },
+ "task.type.import": { cn: "添加数据集", en: "Dataset Add", },
"task.type.copy": { cn: "复制", en: "Copy", },
"task.type.modelimport": { cn: "导入模型", en: "Model Import", },
"task.type.modelcopy": { cn: "复制模型", en: "Copy Model", },
@@ -19,16 +21,19 @@ const task = {
"task.column.duration": { cn: "时长", en: "Duration", },
"task.action.terminate": { cn: "终止", en: "Terminate", },
"task.action.copy": { cn: "复制", en: "Copy", },
+ "task.action.training": { cn: "训练过程", en: "Training Process", },
+ "task.action.training.batch": { cn: "多模型训练过程", en: "Models Training Process", },
"task.action.terminate.confirm.content": { cn: "确认要终止:{name}?", en: "Are you sure to terminate: {name}?", },
- "task.detail.label.train_goal": { cn: "训练目标", en: "Train Classes", },
+ "task.detail.label.train_goal": { cn: "训练目标", en: "Target", },
"task.detail.label.framework": { cn: "算法框架", en: "Network", },
"task.detail.label.create_time": { cn: "创建时间", en: "Created", },
+ "task.detail.label.premodel": { cn: "预训练模型", en: "Pre-Training Model", },
"task.detail.label.backbone": { cn: "骨干网络结构", en: "Backbone", },
- "task.detail.label.hyperparams": { cn: "高级参数", en: "Senior Config", },
"task.detail.label.training.image": { cn: "训练镜像", en: "Training Image", },
"task.detail.label.mining.image": { cn: "挖掘镜像", en: "Mining Image", },
- "task.detail.state.title": { cn: "任务状态", en: "Task State", },
- "task.detail.state.current": { cn: "当前状态", en: "Current State", },
+ "task.detail.label.inference.image": { cn: "推理镜像", en: "Inference Image", },
+ "task.detail.state.title": { cn: "状态", en: "State", },
+ "task.detail.state.current": { cn: "当前进度", en: "Progress", },
"task.detail.error.code": { cn: "失败原因", en: "Error Reason", },
"task.detail.error.desc": { cn: "失败描述", en: "Error Desc", },
"task.detail.label.download.btn": { cn: "下载标注描述文档", en: "Download Label Desc Doc", },
@@ -36,20 +41,23 @@ const task = {
"task.fusion.create.success.msg": { cn: "创建成功", en: "Create Task Success!", },
"task.fusion.form.dataset": { cn: "原数据集", en: "Original Dataset", },
"task.fusion.form.datasets.placeholder": { cn: "请选择数据集", en: "Select/Fusion Datasets", },
- "task.fusion.form.include.label": { cn: "保留标签", en: "Keep Keywords", },
- "task.fusion.form.exclude.label": { cn: "排除标签", en: "Exclude Keywords", },
+ "task.fusion.form.include.label": { cn: "保留类别", en: "Keep Classes", },
+ "task.fusion.form.exclude.label": { cn: "排除类别", en: "Exclude Classes", },
"task.fusion.form.sampling": { cn: "采样数量", en: "Samples", },
"task.train.form.trainsets.label": { cn: "训练集", en: "Train Sets", },
- "task.train.form.testsets.label": { cn: "测试集", en: "Test Sets", },
- "task.train.form.keywords.label": { cn: "训练目标", en: "Train Classes", },
+ "task.train.form.testsets.label": { cn: "验证集", en: "Validation Sets", },
+ "task.train.form.keywords.label": { cn: "训练目标", en: "Target", },
"task.train.form.traintype.label": { cn: "训练类型", en: "Train Type", },
"task.train.form.network.label": { cn: "算法框架", en: "Network", },
"task.train.form.backbone.label": { cn: "骨干网络结构", en: "Backbone", },
- "task.train.form.hyperparam.label": { cn: "高级参数", en: "Senior Config", },
+ "task.train.form.hyperparam.label": { cn: "超参数配置", en: "Hyper Parameters Config", },
+ "task.train.form.hyperparam.label.tip": { cn: "(点击切换展开/收起)", en: " (click to toggle unfold/fold)", },
"task.train.form.traintypes.detect": { cn: "目标检测", en: "Object Detection", },
- "task.train.form.image.label": { cn: "镜像选择", en: "Docker Image", },
+ "task.train.form.image.label": { cn: "训练镜像", en: "Training Image", },
+ "task.inference.form.image.label": { cn: "推理镜像", en: "Inference Image", },
+ "task.mining.form.image.label": { cn: "挖掘镜像", en: "Mining Image", },
"task.train.form.image.placeholder": { cn: "请选择镜像", en: "Please select a docker image", },
- "task.train.form.image.required": { cn: "请选择镜像", en: "Please select a docker image", },
+ "task.train.form.image.required": { cn: "镜像为必选项", en: "Training docker image is required", },
"task.train.total.label": { cn: "共 {total} 个", en: "Total {total} assets", },
"task.train.form.repeatdata.label": { cn: "当数据重复时", en: "Found duplicate data", },
"task.train.form.repeatdata.terminate": { cn: "终止任务", en: "Terminate Task", },
@@ -57,11 +65,10 @@ const task = {
"task.train.form.repeatdata.original": { cn: "采用最初的标注", en: "Use Data With Original Annotation", },
"task.mining.form.model.label": { cn: "模型", en: "Model", },
"task.mining.form.model.required": { cn: "请选择模型", en: "Plese select a model", },
-
+ "task.train.form.keywords.placeholder": { en: "Please select classes", cn: "请选择训练目标", },
"task.mining.form.mining.model.required": { cn: "请选择用于数据挖掘的模型", en: "please select the model used for data mining", },
"task.mining.form.algo.label": { cn: "挖掘算法", en: "Mining Algorithm", },
- "task.mining.form.strategy.label": { cn: "筛选策略", en: "Filter Strategy", },
"task.mining.form.topk.label": { cn: "TOPK", en: "TOP K", },
"task.label.form.type.newer": { cn: "未标注部分", en: "Unlabel", },
"task.mining.form.dataset.label": { cn: "挖掘集", en: "Mining Dataset", },
@@ -85,7 +92,7 @@ const task = {
"task.label.form.member.placeholder": { cn: "请输入标注人员的邮箱", en: "Please input labeller's email", },
"task.label.form.member.labelplatacc": { cn: "请输入当前用户在标注平台上的注册邮箱", en: "Please enter the current user's registered email on the labeling platform", },
- "task.label.form.member.labeltarget": { cn: "请选择用于标注的目标标签,可多选", en: "Please select target keyword for marking, multiple options available", },
+ "task.label.form.member.labeltarget": { cn: "请选择用于标注的目标类别,可多选", en: "Please select classes for marking, multiple options available", },
"task.label.form.member.email.msg": { cn: "请输入正确的邮箱格式", en: "Please input valid EMAIL", },
"task.label.form.target.label": { cn: "标注目标", en: "Label Classes", },
@@ -96,6 +103,9 @@ const task = {
"task.label.form.plat.label": { cn: "标注平台账号", en: "Label Platform Account", },
"task.label.form.plat.go": { cn: "到标注平台注册账号", en: "Label Platform", },
"task.label.form.keep_anno.label": { cn: "保留原标注", en: "Keep Annotations", },
+ "task.label.form.keep_anno.none": { cn: "不保留原标注", en: "No", },
+ "task.label.form.keep_anno.gt": { cn: "保留标准值标注", en: "Keep GT", },
+ "task.label.form.keep_anno.pred": { cn: "保留预测标注", en: "Keep Prediction", },
"task.train.fold": { cn: '收起参数配置', en: 'Fold', },
"task.train.unfold": { cn: '展开参数配置', en: 'Unfold', },
"task.train.parameter.add.label": { cn: '添加自定义参数', en: 'Add Custom Parameter', },
@@ -106,9 +116,13 @@ const task = {
"task.train.gpu.invalid": { cn: 'GPU个数必须在{min}-{max}之间', en: 'GPU Count must between {min} - {max}', },
"task.gpu.tip": { cn: '当前可用GPU个数为 {count}', en: 'Valid GPU count: {count}', },
'task.detail.label.go.platform': { cn: '跳转到标注平台>>', en: 'Go to Label Platform >>' },
+ 'task.detail.label.processing': { cn: '训练过程', en: 'Training Processing' },
+ 'task.detail.label.function': { cn: '训练方式', en: 'Training Function' },
+ 'task.detail.function.live': { cn: '远端训练', en: 'Livecode Training' },
+ 'task.detail.function.local': { cn: '本地训练', en: 'Local Training' },
"task.terminate.label.nodata": { cn: '不获取数据终止', en: 'Terminate', },
"task.terminate.label.withdata": { cn: '获取结果终止', en: 'Terminate & Fetch Result', },
- "task.detail.tensorboard.link.label": { cn: '到TensorBoard查看训练详情', en: 'View in Tensorboard', },
+ "task.detail.tensorboard.link.label": { cn: '去查看>>', en: 'View Training', },
"tip.task.train.model": { cn: '此次训练将会基于选择的模型的基础上继续训练,迭代次数将会累加,目前训练目标需要和模型保持一致', en: 'This training task will base on the model selected, and training target will be ', },
"task.train.form.model.placeholder": { cn: '请选择模型', en: 'Please select a model', },
"task.fusion.header.merge": { cn: '数据集合并', en: 'Dataset Merge', },
@@ -117,25 +131,81 @@ const task = {
"task.fusion.form.merge.include.label": { cn: '合并数据集', en: 'Merge Datasets', },
"task.fusion.form.merge.exclude.label": { cn: '排除数据集', en: 'Exclude Datasets', },
"task.train.form.training.datasets.placeholder": { cn: "请选择数据集", en: "Please select a dataset", },
- "task.train.form.testset.required": { cn: "测试集为必选项", en: "Test dataset is required", },
+ "task.fusion.form.includes.label": { cn: '添加挖掘数据', en: 'Merge Mining Datasets', },
+ "task.fusion.form.excludes.label": { cn: '排除挖掘数据', en: 'Exclude Mining Datasets', },
+ "task.fusion.form.class.include.label": { cn: "选择数据类别", en: "Select Classes", },
+ "task.fusion.form.class.exclude.label": { cn: "排除数据类别", en: "Exclude Classes", },
+ "task.train.form.testset.required": { cn: "验证集为必选项", en: "Validation dataset is required", },
"task.train.form.trainset.required": { cn: "训练集为必选项", en: "Training dataset is required", },
"task.train.form.miningset.required": { cn: "挖掘集为必选项", en: "Mining dataset is required", },
- "task.train.form.test.datasets.placeholder": { cn: "请选择测试集", en: "Please select a test dataset", },
+ "task.train.form.test.datasets.placeholder": { cn: "请选择验证集", en: "Please select a validation dataset", },
"task.origin.dataset": { cn: "来源数据集", en: "Original Dataset", },
"task.detail.include_datasets.label": { cn: "合并数据集", en: "Merge Datasets", },
"task.detail.exclude_datasets.label": { cn: "排除数据集", en: "Exclude Datasets", },
- "task.detail.include_labels.label": { cn: "保留标签", en: "Include Keywords", },
- "task.detail.exclude_labels.label": { cn: "排除标签", en: "Exclude Keywords", },
+ "task.detail.include_labels.label": { cn: "保留类别", en: "Include Classes", },
+ "task.detail.exclude_labels.label": { cn: "排除类别", en: "Exclude Classes", },
"task.detail.samples.label": { cn: "采样数", en: "Sampling Count", },
"task.detail.source.sys": { cn: "系统自动生成项目训练集", en: "Project training dataset generated by system", },
"task.infer.gpu.tip": {
- cn: "请输入每个模型推理分配到的GPU个数,目前已选中{selected}/{total}",
- en: "Input GPU count for each model, currently {selected}/{total} selected.",
+ cn: "请输入单个模型在单个数据集上推理需要的GPU个数,目前已选中{selected}/{total}",
+ en: "Input GPU count for single model infer single dataset, currently {selected}/{total} selected.",
},
"task.inference.unmatch.keywrods": {
- cn: "选中的模型中的训练目标{keywords}不在当前用户的标签列表中,推理对这些目标无效",
+ cn: "选中的模型中的训练目标{keywords}不在当前用户的类别列表中,推理对这些目标无效",
en: "Inference partial invalidity by {keywords} for selected model out of KEYWORD LIST of current user.",
},
+ "task.train.live.title": { en: "Live Code Configure", cn: "远程代码配置", },
+ "task.train.live.url": { en: "GitHub Repo. URL", cn: "GitHub仓库地址", },
+ "task.train.live.id": { en: "Commit ID", cn: "提交ID", },
+ "task.train.live.config": { en: "Config Filename", cn: "配置文件名称", },
+ "task.train.live.url.placeholder": {
+ en: "Plese input your GitHub repo. URL, http(s) supported",
+ cn: "请输入GitHub仓库地址, 请使用http或https协议,暂不支持git协议",
+ },
+ "task.train.live.id.placeholder": { en: "Please input your commit ID", cn: "请输入提交ID", },
+ "task.train.live.config.placeholder": {
+ en: "Please input relative path to your config filename, e.g. config/white.yaml",
+ cn: "请输入配置文件名称的相对路径, 如 config/white.yaml",
+ },
+ "task.train.form.platform.label": { en: "Use Openpai", cn: "使用Openpai", },
+ "task.train.device.local": { en: "Local", cn: "本地训练", },
+ "task.train.device.openpai": { en: "OpenPAI", cn: "OpenPAI训练", },
+ "task.train.export.format": { en: "Format For Training", cn: "数据导出格式", },
+ "task.infer.diagnose.tip": { en: "Are Models not yet test?", cn: "尚未对模型进行测试?", },
+ "task.train.action.duplicated": { en: "Check Duplication", cn: "检测重复性", },
+ "task.train.duplicated.option.train": { en: "duplicated as trainset data", cn: "重复数据仅用于训练集", },
+ "task.train.duplicated.option.validation": { en: "duplicated as validation data", cn: "重复数据仅用于验证集", },
+ "task.train.duplicated.tip": { en: "{duplicated} assets duplicated, please select strategy: ", cn: "检测到训练集和验证集有{duplicated}张重复数据,请选择策略:", },
+ "task.train.action.duplicated.no": { en: "None of duplicated assets", cn: "无重复数据", },
+ "task.train.action.duplicated.all": {
+ en: "Duplicated completely, please select another training dataset or validation dataset.",
+ cn: "数据完全重复,请重新选择训练集或验证集",
+ },
+ "task.train.preprocess.title": { en: "Image Preprocess", cn: "图像前处理", },
+ "task.train.preprocess.resize": { en: "Maximum Side Length Resize", cn: "最长边长缩放", },
+ "task.train.preprocess.resize.tip": {
+ en: "Images will resize as original scale which longest width/height equal to input value",
+ cn: "图像将按原始比例缩放至最长边为设置值",
+ },
+ "task.train.preprocess.resize.placeholder": { en: "Max width/height", cn: "最大高/宽", },
+ "task.filter.includes.placeholder": { en: "Please select inclusion classes", cn: "请选择保留的类别", },
+ "task.filter.excludes.placeholder": { en: "Please select exclusion classes", cn: "请选择需要排除掉的类别", },
+ "task.state": { en: "Task Status", cn: "任务状态", },
+ "task.detail.terminated.label": { en: "Terminated", cn: "终止", },
+ "task.detail.terminated": { en: "Terminated by user", cn: "用户手动终止", },
+ "task.inference.header.tip": {
+ en: "Inference result dataset will generate prediction, and inherit original testing set's GT.",
+ cn: "推理结果数据集将生成新的预测标注,并包含原测试集的基准值标注。",
+ },
+ "task.label.header.tip": {
+ en: "Labelling result dataset will generate GT.",
+ cn: "标注结果数据集会生成新的标准值标注。",
+ },
+ 'task.merge.type.label': { en: 'Merge Type', cn: '合并方式' },
+ 'task.merge.type.new': { en: 'Generate a new dataset', cn: '生成新数据集' },
+ 'task.merge.type.exist': { en: 'Generate a version for original dataset', cn: '在原数据集上生成新版本' },
+ 'task.train.btn.calc.negative': { en: 'Calculate Positive/Negative Samples', cn: '计算正负样本' },
+ 'task.panel.settings.advanced': { en: 'Advanced Settings', cn: '高级设置' },
}
export default task
diff --git a/ymir/web/src/locales/modules/tip.ts b/ymir/web/src/locales/modules/tip.ts
index f8ce946771..34198a3af1 100644
--- a/ymir/web/src/locales/modules/tip.ts
+++ b/ymir/web/src/locales/modules/tip.ts
@@ -12,28 +12,28 @@ const tip = {
en: "Image description for type, function etc.",
},
"tip.task.fusion.includelable": {
- cn: "期望生成的数据集包含选中的标签值",
- en: "Filtering tips: the generated dataset expect to contain the selected keyword",
+ cn: "期望生成的数据集包含选中的类别",
+ en: "Filtering tips: the generated dataset expect to contain the selected classes",
},
"tip.task.fusion.excludelable": {
- cn: "期望生成的数据集不包含选中的标签值",
- en: "Excluding tips: the generated dataset expect to not contain the selected keyword",
+ cn: "期望生成的数据集不包含选中的类别",
+ en: "Excluding tips: the generated dataset expect to not contain the selected classes",
},
"tip.task.fusion.sampling": {
cn: "期望的最大采样数量",
en: "Samples expected.",
},
"tip.task.filter.testsets": {
- cn: "训练集和测试集的图片不可重复",
- en: "The images of the training set and the testing set cannot be duplicated.",
+ cn: "训练集和验证集的图片不可重复",
+ en: "The images of the training set and the validation set cannot be duplicated.",
},
"tip.task.filter.keywords": {
- cn: "训练目标标签必须同时包含在训练集和测试集中",
- en: "Both the training and testing sets must contain target keyword",
+ cn: "训练目标类别必须同时包含在训练集和验证集中",
+ en: "Both the training and validation sets must contain target classes",
},
"tip.task.train.image": {
cn: "平台提供默认训练镜像,同时支持开发者自主开发并接入。镜像会附带默认配置参数,可按需调整。",
- en: "Default training image supported, and you can add your own training image. Training docker image have its own senior config, you can modify it for training task.",
+ en: "Default training image supported, and you can add your own training docker image. Training docker image have its own senior config, you can modify it for training task.",
},
"tip.task.mining.image": {
cn: "平台提供默认挖掘镜像,同时支持开发者自主开发并接入。镜像会附带默认配置参数,可按需调整。",
@@ -51,13 +51,21 @@ const tip = {
cn: "训练镜像中需传入的运行参数,默认为最佳推荐配置",
en: "Hyperparameter tips: the operation parameters to be entered in the training docker, the default value is the best recommended configuration",
},
- "tip.task.filter.model": {
+ "tip.iteration.initmodel": {
cn: "初始模型用于第一轮迭代时的数据挖掘,可以通过导入或者训练添加。",
en: "The initial model is used for data mining during the first iteration and can be added by importing or training.",
},
+ "tip.task.mining.dataset": {
+ cn: "待挖掘的目标数据集",
+ en: "Dataset for mining",
+ },
+ "tip.task.filter.model": {
+ cn: "本次挖掘出来的数据可用于该选中模型效果的提升",
+ en: "The mining data can used for optimizing the model selected.",
+ },
"tip.task.filter.strategy": {
- cn: "用户自定义挖掘结果数据集的大小,即希望保留TopK个最有利于模型优化的数据。在选择多个数据集时,由于可能存在重复数据,合并后的结果小于所选数据集之和,当用户自定义TopK值大于合并后的数据集大小时,则返回全部数据。",
- en: "User-defined size of the mined result dataset, i.e., you want to keep the TopK data that are most conducive to model optimization.When multiple datasets are selected, the merged result may be smaller than the sum of the selected datasets due to the possible existence of duplicate data. When the user-defined TopK value is larger than the size of the merged dataset, all data are returned.",
+ cn: "用户自定义挖掘结果数据集的大小,即保留得分最高的K个最有利于模型优化的数据。",
+ en: "User-defined size of the mined result dataset, you want to keep the TopK data that are most conducive to model optimization.",
},
"tip.task.filter.newlable": {
cn: "通过所选模型对数据集进行推理,产生新的标注结果",
@@ -92,30 +100,41 @@ const tip = {
en: "The account can be used to view the labeling progress on the labeling platform, please register in advance",
},
"tip.task.filter.labeltarget": {
- cn: "仅支持在当前用户标签列表中选择,如果当前列表没有期望标注的目标标签,请前往标签列表添加",
- en: "Only support the current user‘s keyword list to choose,if the current list does not have the target keyword, please go to the keyword list to add ",
+ cn: "仅支持在当前用户类别中选择,如果没有期望标注的目标类别,请前往类别管理添加",
+ en: "Only support the current user‘s classes, please add if user's classes does not include the target classes",
},
"tip.task.filter.datasets": {
cn: "公共数据集为系统内置数据集,支持用户复用",
en: "Dataset tips: public dataset is the system built-in dataset, support user to reuse",
},
"tip.task.filter.alias": {
- cn: "和主名表示同一类标签,当导入的数据集标签为别名时,界面展示为主名",
- en: "The same type of keyword as the main name, when the imported dataset contains an alias keyword, the interface shows the keyword's main name",
+ cn: "和主名表示同一类类别,当导入的数据集类别为别名时,界面展示为主名",
+ en: "As same meaning as the main class, when the imported dataset contains an alias, the main class show",
},
"project.add.trainset.tip": {
cn: '每轮迭代都会更新该训练集的训练数据,需要包含目标标注',
en: 'Classes annotations required, training data will update in every iterations',
},
"project.add.testset.tip": {
- cn: '用于指导训练集进行训练和测试,可获得更客观的模型效果评估结果,该数据集需要有标注。',
- en: 'It is used to guide the training set for training and testing, which can obtain more objective results for model effect evaluation. This dataset needs to be annotated.',
+ cn: '用于指导训练集进行训练和验证,可获得更客观的模型效果评估结果,该数据集需要有标注。',
+ en: 'It is used to guide the training set for training and validation, which can obtain more objective results for model effect evaluation. This dataset needs to be annotated.',
},
"project.add.miningset.tip": {
cn: '一般情况下无标注且数据量大,通过数据挖掘在该数据集下找到更贴合目标业务场景的数据。',
en: 'Generally unlabeled and with a large amount of data, data mining is used to find data that better fits the target business scenario under that data set.',
},
-
+ "tip.train.export.format": {
+ cn: '导出给训练镜像使用的数据格式,图像格式包含raw/lmdb, 标注格式有none/ark/voc/ls_json。请根据镜像选择相应的数据格式。',
+ en: 'Export format for training docker image. assets format: raw/lmdb, annotations format none/ark/voc/ls_json. Please choose one matched your training docker image.',
+ },
+ 'tip.task.merge.include': {
+ cn: '选择要统一合并的其他数据集,可多选',
+ en: 'Select datasets to merged, multiple selection',
+ },
+ 'tip.task.merge.exclude': {
+ cn: '选择不期望出现在结果数据集中的数据,可多选',
+ en: 'Select datasets for exclude data, multiple selection',
+ },
}
export default tip
diff --git a/ymir/web/src/locales/modules/user.ts b/ymir/web/src/locales/modules/user.ts
index e654ed8262..398d5c9a2f 100644
--- a/ymir/web/src/locales/modules/user.ts
+++ b/ymir/web/src/locales/modules/user.ts
@@ -60,6 +60,7 @@ const user = {
"user.state.deactived": { cn: "已注销", en: "Closed", },
"user.signup.success": { cn: "注册成功,正在等待管理员审核", en: "Registry success, please waiting for approving.", },
"user.signup.success.active": { cn: "注册成功,请登录", en: "Registry success, please login.", },
+ "user.settings": { cn: "个人中心", en: "User Settings", },
}
export default user
diff --git a/ymir/web/src/models/__test__/dataset.test.js b/ymir/web/src/models/__test__/dataset.test.js
index e83b2d8018..671f30374f 100644
--- a/ymir/web/src/models/__test__/dataset.test.js
+++ b/ymir/web/src/models/__test__/dataset.test.js
@@ -2,7 +2,7 @@ import dataset from "../dataset"
import { put, putResolve, call, select } from "redux-saga/effects"
import { errorCode, normalReducer, product, products } from './func'
import { toFixed } from '@/utils/number'
-import { transferDatasetGroup, transferDataset, states } from '@/constants/dataset'
+import { transferDatasetGroup, transferDataset, transferAsset, states } from '@/constants/dataset'
jest.mock('umi', () => {
return {
@@ -71,7 +71,6 @@ describe("models: dataset", () => {
errorCode(dataset, 'createDataset')
errorCode(dataset, 'updateDataset')
errorCode(dataset, 'getInternalDataset')
- errorCode(dataset, 'getHotDatasets', 10034, [])
const gid = 534234
const items = products(4)
const datasets = { items, total: items.length }
@@ -92,7 +91,7 @@ describe("models: dataset", () => {
const state = {
datasets: {},
}
- const initQuery = { name: "", type: "", time: 0, offset: 0, limit: 20 }
+ const initQuery = { name: "", type: "", current: 1, time: 0, offset: 0, limit: 20 }
const expected = {
query: { ...initQuery },
@@ -165,17 +164,18 @@ describe("models: dataset", () => {
const saga = dataset.effects.batchDatasets
const creator = {
type: "batchDatasets",
- payload: { ids: '1,2' },
+ payload: { pid: 23434, ids: [1, 2] },
}
const recieved = [1, 3, 4].map(id => ds(id))
const expected = recieved.map(item => transferDataset(item))
const generator = saga(creator, { put, call })
generator.next()
- const end = generator.next({
+ generator.next({
code: 0,
result: recieved,
})
+ const end = generator.next()
expect(expected).toEqual(end.value)
expect(end.done).toBe(true)
@@ -186,18 +186,19 @@ describe("models: dataset", () => {
type: "getAssetsOfDataset",
payload: {},
}
- const expected = { items: [1, 2, , 3, 4], total: 4 }
+ const result = { items: products(4), total: 4 }
+ const expected = { items: result.items.map(item => transferAsset(item)), total: 4 }
const generator = saga(creator, { put, call })
generator.next()
generator.next({
code: 0,
- result: expected,
+ result,
})
const end = generator.next()
- equalObject(expected, end.value)
expect(end.done).toBe(true)
+ expect(end.value).toEqual(expected)
})
it("effects: queryAllDatasets -> from remote", () => {
const saga = dataset.effects.queryAllDatasets
@@ -207,8 +208,9 @@ describe("models: dataset", () => {
}
const expected = { items: [1, 2, 3, 4], total: 4 }
- const generator = saga(creator, { put, call })
+ const generator = saga(creator, { put, call, select })
generator.next()
+ generator.next(false)
generator.next(expected)
const end = generator.next()
@@ -225,6 +227,7 @@ describe("models: dataset", () => {
const generator = saga(creator, { put, call, select })
generator.next()
+ generator.next(false)
const end = generator.next(expected)
expect(end.value).toEqual(expected)
@@ -240,6 +243,7 @@ describe("models: dataset", () => {
const generator = saga(creator, { put, call, select })
generator.next()
+ generator.next(false)
generator.next([])
generator.next({ items: expected, total: expected.length })
const end = generator.next()
@@ -326,15 +330,12 @@ describe("models: dataset", () => {
type: "getAsset",
payload: { hash: 'identify_hash_string' },
}
- const expected = {
+ const result = {
hash: 'identify_hash_string', width: 800, height: 600,
"size": 0,
"channel": 3,
"timestamp": "2021-09-28T08:26:53.088Z",
"url": "string",
- "annotations": [
- {}
- ],
"metadata": {},
"keywords": [
"string"
@@ -345,11 +346,11 @@ describe("models: dataset", () => {
generator.next()
generator.next({
code: 0,
- result: expected,
+ result,
})
const end = generator.next()
- equalObject(expected, end.value)
+ expect(end.value).toEqual(transferAsset(result))
expect(end.done).toBe(true)
})
it("effects: delDataset", () => {
@@ -386,7 +387,7 @@ describe("models: dataset", () => {
result: expected,
})
- equalObject(expected, end.value)
+ expect(end.value).toEqual(expected)
expect(end.done).toBe(true)
})
it("effects: updateDataset", () => {
@@ -430,7 +431,6 @@ describe("models: dataset", () => {
generator.next()
const d = generator.next(versions)
const end = generator.next()
- const updated = d.value.payload.action.payload
expect(end.value).toEqual(expected)
expect(end.done).toBe(true)
@@ -451,9 +451,8 @@ describe("models: dataset", () => {
generator.next()
const d = generator.next(versions)
const end = generator.next()
- const updated = d.value.payload.action.payload
- expect(updated).toEqual(versions)
+ expect(end.value).toEqual(versions)
expect(end.done).toBe(true)
})
it("effects: updateDatasetState -> normal success", () => {
@@ -465,7 +464,10 @@ describe("models: dataset", () => {
}
const creator = {
type: "updateDatasets",
- payload: { hash1: { id: 1, state: 2, result_state: 0, percent: 0.45 }, hash7: { id: 7, state: 3, result_state: 1, percent: 1 } },
+ payload: {
+ hash1: { id: 1, state: 2, result_dataset: {id: 1}, result_state: 0, percent: 0.45 },
+ hash7: { id: 7, state: 3, result_state: 1, percent: 1 }
+ },
}
const expected = {
'1': ds(1, 2, 0, 0.45),
@@ -475,70 +477,6 @@ describe("models: dataset", () => {
generator.next()
const d = generator.next(datasets)
const end = generator.next()
- const updated = d.value.payload.action.payload
-
- expect(updated).toEqual(expected)
- expect(end.done).toBe(true)
- })
- it("effects: getHotDatasets -> get stats result success-> batch datasets success", () => {
- const saga = dataset.effects.getHotDatasets
- const creator = {
- type: "getHotDatasets",
- payload: { limit: 3 },
- }
- const result = [[1, 34], [1001, 31], [23, 2]]
- const datasets = [{ id: 1 }, { id: 1001 }, { id: 23 }]
- const expected = [{ id: 1, count: 34 }, { id: 1001, count: 31 }, { id: 23, count: 2 }]
-
- const generator = saga(creator, { put, call, select })
- generator.next()
- generator.next({
- code: 0,
- result,
- })
- const end = generator.next(datasets)
-
- expect(end.value).toEqual(expected)
- expect(end.done).toBe(true)
- })
- it("effects: getHotDatasets -> get stats result success-> batch datasets failed", () => {
- const saga = dataset.effects.getHotDatasets
- const creator = {
- type: "getHotDatasets",
- payload: { limit: 3 },
- }
- const result = [[1, 34], [1001, 31], [23, 2]]
- const datasets = undefined
- const expected = []
-
- const generator = saga(creator, { put, call, select })
- generator.next()
- generator.next({
- code: 0,
- result,
- })
- const end = generator.next(datasets)
-
- expect(end.value).toEqual(expected)
- expect(end.done).toBe(true)
- })
- it("effects: getHotDatasets -> stats result = []", () => {
- const saga = dataset.effects.getHotDatasets
- const creator = {
- type: "getHotDatasets",
- payload: { limit: 4 },
- }
- const result = []
- const expected = []
-
- const generator = saga(creator, { put, call, select })
- generator.next()
- const end = generator.next({
- code: 0,
- result,
- })
-
- expect(end.value).toEqual(expected)
expect(end.done).toBe(true)
})
it("effects: getInternalDataset", () => {
@@ -562,22 +500,22 @@ describe("models: dataset", () => {
expect(end.done).toBe(true)
})
- it("effects: compare", () => {
- const saga = dataset.effects.compare
- const item = () => ({ ap: Math.random()})
- const list = (list, it) => list.reduce((p, c) => ({ ...p, [c]: it ? it : item()}), {})
+ it("effects: evaluate", () => {
+ const saga = dataset.effects.evaluate
+ const item = () => ({ ap: Math.random() })
+ const list = (list, it) => list.reduce((p, c) => ({ ...p, [c]: it ? it : item() }), {})
const keywords = ['dog', 'cat', 'person']
const ious = [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95].map(n => toFixed(n, 2))
const iitems = () => ({
- ci_evaluations: list(keywords),
- ci_everage_evaluations: item(),
- })
+ ci_evaluations: list(keywords),
+ ci_everage_evaluations: item(),
+ })
const expected = {
iou_evaluations: list(ious, iitems()),
iou_everage_evaluations: iitems(),
}
const creator = {
- type: "compare",
+ type: "evaluate",
payload: { projectId: 51234, gt: 1324536, datasets: [534243234, 64311234], confidence: 0.6 },
}
diff --git a/ymir/web/src/models/__test__/image.test.js b/ymir/web/src/models/__test__/image.test.js
index a8088de858..c91df83739 100644
--- a/ymir/web/src/models/__test__/image.test.js
+++ b/ymir/web/src/models/__test__/image.test.js
@@ -1,6 +1,7 @@
import image from "../image"
import { put, call, select } from "redux-saga/effects"
import { errorCode } from './func'
+import { transferImage } from "../../constants/image"
function equalObject(obj1, obj2) {
expect(JSON.stringify(obj1)).toBe(JSON.stringify(obj2))
@@ -50,6 +51,7 @@ describe("models: image", () => {
}
const images = products(9).map(image => ({ id: image, configs: [{ config: { anchor: '12,3,4'}, type: 1 }]}))
const result = { items: images, total: images.length }
+ const expected = { items: images.map(img => transferImage(img)), total: images.length}
const generator = saga(creator, { put, call })
generator.next()
@@ -59,7 +61,7 @@ describe("models: image", () => {
})
const end = generator.next()
- expect(end.value).toEqual({ items: images.map((image, index) => ({ ...image, functions: index === images.length ? [] : [1]})), total: images.length })
+ expect(end.value).toEqual(expected)
expect(end.done).toBe(true)
})
it("effects: getShareImages -> success", () => {
@@ -69,7 +71,7 @@ describe("models: image", () => {
payload: {},
}
const images = products(9).map(image => ({ id: image, configs: [{ config: { anchor: '12,3,4'}, type: 1 }]}))
- const expected = { items: images, total: images.length }
+ const expected = { items: images.map(image => transferImage(image)), total: images.length }
const generator = saga(creator, { put, call })
generator.next()
@@ -99,7 +101,7 @@ describe("models: image", () => {
})
const end = generator.next()
- expect(end.value).toEqual({ ...expected, functions: [] })
+ expect(end.value).toEqual(transferImage(expected))
expect(end.done).toBe(true)
})
it("effects: delImage", () => {
@@ -142,11 +144,12 @@ describe("models: image", () => {
})
it("effects: updateImage", () => {
const saga = image.effects.updateImage
+ const payload = {id: 10011, name: 'new_image_name'}
const creator = {
type: "updateImage",
- payload: {id: 10011, name: 'new_image_name'},
+ payload,
}
- const expected = {id: 10011, name: 'new_image_name'}
+ const expected = transferImage(payload)
const generator = saga(creator, { put, call })
generator.next()
diff --git a/ymir/web/src/models/__test__/iteration.test.js b/ymir/web/src/models/__test__/iteration.test.js
index 4568490074..8cf8396de3 100644
--- a/ymir/web/src/models/__test__/iteration.test.js
+++ b/ymir/web/src/models/__test__/iteration.test.js
@@ -1,7 +1,7 @@
import iteration from "../iteration"
import { put, putResolve, call, select } from "redux-saga/effects"
import { product, products, errorCode, normalReducer, generatorCreator } from "./func"
-import { Stages, transferIteration } from '@/constants/project'
+import { Stages, transferIteration } from '@/constants/iteration'
put.resolve = putResolve
@@ -12,11 +12,10 @@ function equalObject(obj1, obj2) {
describe("models: iteration", () => {
const createGenerator = generatorCreator(iteration)
normalReducer(iteration, 'UPDATE_ITERATIONS', { id: 13424, iterations: product(34) }, { 13424: product(34) }, 'iterations', {})
- normalReducer(iteration, 'UPDATE_ITERATION', product(100434), product(100434), 'iteration', {})
+ normalReducer(iteration, 'UPDATE_ITERATION', product(100434), { 100434: product(100434) }, 'iteration', {})
normalReducer(iteration, 'UPDATE_CURRENT_STAGE_RESULT', product(100435), product(100435), 'currentStageResult', {})
errorCode(iteration, "getIterations")
- errorCode(iteration, "getIteration")
errorCode(iteration, "createIteration")
errorCode(iteration, "updateIteration")
@@ -30,42 +29,7 @@ describe("models: iteration", () => {
code: 0,
result: iterations,
})
- const end = generator.next()
-
- expect(end.value).toEqual(expected)
- expect(end.done).toBe(true)
- })
- it("effects: getIterations -> success -> get more info with data.", () => {
- const pid = 64321
- const iterations = products(3).map(({ id }) => ({
- id,
- mining_input_dataset_id: id,
- mining_output_dataset_id: id,
- label_output_dataset_id: id,
- training_input_dataset_id: id,
- training_output_model_id: id,
- testing_dataset_id: id,
- }))
- const datasets = products(3)
- const models = products(3)
- const expected = iterations.map(i => ({
- ...transferIteration(i),
- trainUpdateDataset: product(i.id),
- miningDataset: product(i.id),
- miningResultDataset: product(i.id),
- labelDataset: product(i.id),
- trainingModel: product(i.id),
- testDataset: product(i.id),
- }))
-
- const generator = createGenerator('getIterations', { id: pid, more: true })
generator.next()
- generator.next({
- code: 0,
- result: iterations,
- })
- generator.next(datasets)
- generator.next(models)
const end = generator.next()
expect(end.value).toEqual(expected)
@@ -80,10 +44,8 @@ describe("models: iteration", () => {
const generator = createGenerator('getIteration', { pid, id })
generator.next()
- generator.next({
- code: 0,
- result,
- })
+ generator.next()
+ generator.next(expected)
const end = generator.next()
expect(end.value).toEqual(expected)
@@ -97,8 +59,7 @@ describe("models: iteration", () => {
const generator = createGenerator('getStageResult', { id: pid, stage, })
generator.next()
- generator.next(result)
- const end = generator.next()
+ const end = generator.next(result)
expect(end.value).toEqual(expected)
expect(end.done).toBe(true)
@@ -111,67 +72,27 @@ describe("models: iteration", () => {
const generator = createGenerator('getStageResult', { id: pid, stage, force: true })
generator.next()
- generator.next(result)
- const end = generator.next()
+ const end = generator.next(result)
expect(end.value).toEqual(expected)
expect(end.done).toBe(true)
})
- it("effects: updateCurrentStageResult -> progress", () => {
- const ds = (id, state, result_state, progress) => ({
- id,
- task: { hash: `hash${id}`, state, percent: progress, },
- taskState: state,
- state: result_state, progress
- })
-
- const result = ds(1, 2, 0, 0.20)
- const task1 = { hash1: { id: 1, state: 2, result_state: 0, percent: 0.45 }, hash7: { id: 7, state: 3, result_state: 1, percent: 1 } }
- const expected = ds(1, 2, 0, 0.45)
-
- const generator = createGenerator('updateCurrentStageResult', task1)
- generator.next()
- const d = generator.next(result)
- const end = generator.next()
- const updated = d.value.payload.action.payload
-
- expect(updated).toEqual(expected)
- expect(end.done).toBe(true)
- })
- it("effects: updateCurrentStageResult -> state change.", () => {
- const ds = (id, state, result_state, progress) => ({
- id,
- task: { hash: `hash${id}`, state, percent: progress, },
- taskState: state,
- state: result_state, progress
- })
-
- const result = ds(1, 2, 0, 0.20)
- const task = { hash1: { id: 1, state: 3, result_state: 1, percent: 1 } }
- const expected = { ...ds(1, 3, 1, 1), needReload: true }
-
- const generator = createGenerator('updateCurrentStageResult', task)
- generator.next()
- const d = generator.next(result)
- const end = generator.next()
- const updated = d.value.payload.action.payload
-
- expect(updated).toEqual(expected)
- expect(end.done).toBe(true)
- })
it("effects: createIteration", () => {
const id = 10015
- const expected = { id, name: "new_iteration_name" }
-
+ const result = { id, name: "new_iteration_name" }
+ const expected = transferIteration(result)
const generator = createGenerator('createIteration', { name: "new_iteration_name", type: 1 })
generator.next()
- const end = generator.next({
+ generator.next({
code: 0,
- result: expected,
+ result,
})
+ generator.next()
+ generator.next([])
+ const end = generator.next()
- equalObject(expected, end.value)
+ expect(end.value).toEqual(expected)
expect(end.done).toBe(true)
})
it("effects: updateIteration", () => {
@@ -181,47 +102,18 @@ describe("models: iteration", () => {
const generator = createGenerator('updateIteration', origin)
generator.next()
- const end = generator.next({
+ generator.next({
code: 0,
result: expected,
})
-
- expect(end.value).toEqual(expected)
- expect(end.done).toBe(true)
- })
-
- it("effects: getIterationStagesResult -> get datasets/model for iteration", () => {
- const pid = 62314
- const iter = {
- id: 5349,
- projectId: pid,
- miningSet: 5340,
- miningResult: 5341,
- labelSet: 5342,
- trainUpdateSet: 5343,
- model: 34234,
- }
- const datasets = [product(5340), product(5341), product(5342), product(5343)]
- const model = product(32234)
-
- const expected = {
- ...iter,
- iminingSet: product(5340),
- iminingResult: product(5341),
- ilabelSet: product(5342),
- itrainUpdateSet: product(5343),
- imodel: product(32234),
- }
- const generator = createGenerator('getIterationStagesResult', iter)
-
generator.next()
- generator.next(datasets)
- const end = generator.next(model)
+ generator.next([])
+ const end = generator.next()
expect(end.value).toEqual(expected)
expect(end.done).toBe(true)
})
- it('effects: setCurrentStageResult -> success', ()=> {
+ it('effects: setCurrentStageResult -> success', () => {
const model = product(523464)
const expected = model
const generator = createGenerator('setCurrentStageResult', model)
diff --git a/ymir/web/src/models/__test__/keyword.test.js b/ymir/web/src/models/__test__/keyword.test.js
index d4f691b492..f7f5903cf4 100644
--- a/ymir/web/src/models/__test__/keyword.test.js
+++ b/ymir/web/src/models/__test__/keyword.test.js
@@ -31,7 +31,7 @@ describe("models: keyword", () => {
const kw = { name: 'cat', aliases: ['kitty', 'hunney'] }
const creator = {
type: "updateKeywords",
- payload: { keywords: [kw], dry_run: false},
+ payload: { keywords: [kw], dry_run: false },
}
const keywords = [kw]
const response = { code: 0, result: keywords }
@@ -61,13 +61,13 @@ describe("models: keyword", () => {
})
it("effects: getRecommendKeywords", () => {
const saga = keyword.effects.getRecommendKeywords
- const kw = [['cat', 12], ['dog', 6], ['person', 3]]
- const expected = kw.map(item => item[0])
+ const result = [{ legend: 'cat', count: 12 }, { legend: 'dog', count: 6 }, { legend: 'person', count: 3 }]
+ const expected = result.map(item => item.legend)
const creator = {
type: "getRecommendKeywords",
- payload: kw,
+ payload: { datasets: [34, 4], limit: 5 },
}
- const response = { code: 0, result: kw }
+ const response = { code: 0, result }
const generator = saga(creator, { put, call, select })
generator.next()
diff --git a/ymir/web/src/models/__test__/model.test.js b/ymir/web/src/models/__test__/model.test.js
index fa79fbf6fa..3fa6fa9592 100644
--- a/ymir/web/src/models/__test__/model.test.js
+++ b/ymir/web/src/models/__test__/model.test.js
@@ -2,6 +2,7 @@ import model from "../model"
import { put, putResolve, select, call } from "redux-saga/effects"
import { errorCode, generatorCreator, product, products, list, response } from './func'
import { transferModelGroup, transferModel, states } from '@/constants/model'
+import { transferAnnotation } from '@/constants/dataset'
put.resolve = putResolve
@@ -135,6 +136,7 @@ describe("models: model", () => {
errorCode(model, 'delModel')
errorCode(model, 'importModel')
errorCode(model, 'updateModel')
+ errorCode(model, 'setRecommendStage')
errorCode(model, 'verify')
errorCode(model, 'getModelsByMap', 10025, { keywords: [], kmodels: {} })
errorCode(model, 'getModelVersions', { id: 235234, force: true })
@@ -252,6 +254,27 @@ describe("models: model", () => {
expect(end.value.id).toBe(expected.id)
expect(end.done).toBe(true)
})
+ it("effects: setRecommendStage", () => {
+ const saga = model.effects.setRecommendStage
+ const modelId = 13412
+ const stage = 23234
+ const params = { modelId, stage, }
+ const expected = md(modelId)
+ const creator = {
+ type: "setRecommendStage",
+ payload: params,
+ }
+
+ const generator = saga(creator, { put, call })
+ const start = generator.next()
+ const end = generator.next({
+ code: 0,
+ result: expected,
+ })
+
+ expect(end.value).toEqual(transferModel(expected))
+ expect(end.done).toBe(true)
+ })
it("effects: verify", () => {
const saga = model.effects.verify
const id = 620
@@ -260,10 +283,11 @@ describe("models: model", () => {
type: "verify",
payload: { id, urls: [url] },
}
+ const boxes = [{ box: { x: 20, y: 52, w: 79, h: 102 }, keyword: 'cat', score: 0.8 }]
const expected = {
model_id: id,
annotations: [
- { img_url: url, detections: [{ box: { x: 20, y: 52, w: 79, h: 102 } }] }
+ { img_url: url, detection: boxes }
]
}
@@ -274,7 +298,7 @@ describe("models: model", () => {
result: expected,
})
- expect(end.value.model_id).toBe(id)
+ expect(end.value).toEqual(boxes.map(transferAnnotation))
expect(end.done).toBe(true)
})
// getModelsByMap
diff --git a/ymir/web/src/models/__test__/project.test.js b/ymir/web/src/models/__test__/project.test.js
index b528fda656..272fd054b4 100644
--- a/ymir/web/src/models/__test__/project.test.js
+++ b/ymir/web/src/models/__test__/project.test.js
@@ -49,7 +49,7 @@ describe("models: project", () => {
type: "getProjects",
payload: {},
}
- const projects = products(9)
+ const projects = products(6)
const expected = projects.map(item => transferProject(item))
const result = { items: projects, total: projects.length }
@@ -84,6 +84,7 @@ describe("models: project", () => {
code: 0,
result: expected,
})
+ generator.next()
const end = generator.next()
expect(end.value).toEqual(transferProject(expected))
@@ -162,8 +163,8 @@ describe("models: project", () => {
})
const end = generator.next()
- equalObject(expected, end.value)
expect(end.done).toBe(true)
+ expect(end.value).toEqual(transferProject(expected))
})
it("effects: checkStatus", () => {
const saga = project.effects.checkStatus
diff --git a/ymir/web/src/models/__test__/task.test.js b/ymir/web/src/models/__test__/task.test.js
index dccb1039f7..8076bf113e 100644
--- a/ymir/web/src/models/__test__/task.test.js
+++ b/ymir/web/src/models/__test__/task.test.js
@@ -11,10 +11,12 @@ describe("models: task", () => {
errorCode(task, 'getTask')
errorCode(task, 'deleteTask')
errorCode(task, 'updateTask')
- errorCode(task, 'createFusionTask')
- errorCode(task, 'createLabelTask')
- errorCode(task, 'createTrainTask')
- errorCode(task, 'createMiningTask')
+ errorCode(task, 'fusion')
+ errorCode(task, 'merge')
+ errorCode(task, 'filter')
+ errorCode(task, 'label')
+ errorCode(task, 'train')
+ errorCode(task, 'mine')
errorCode(task, 'stopTask')
it("reducers: UPDATE_TASKS, UPDATE_TASK", () => {
@@ -42,10 +44,30 @@ describe("models: task", () => {
expect(result.task.id).toBe(expectedId)
})
- it("effects: createFusionTask", () => {
- const saga = task.effects.createFusionTask
+ it("effects: fusion", () => {
+ const saga = task.effects.fusion
const creator = {
- type: "createFusionTask",
+ type: "fusion",
+ payload: {},
+ }
+ const expected = "ok"
+
+ const generator = saga(creator, { put, call })
+ const start = generator.next()
+ generator.next({
+ code: 0,
+ result: expected,
+ })
+ const end = generator.next()
+
+ expect(end.value).toBe(expected)
+ expect(end.done).toBe(true)
+ })
+
+ it("effects: merge", () => {
+ const saga = task.effects.merge
+ const creator = {
+ type: "merge",
payload: {},
}
const expected = "ok"
@@ -56,16 +78,36 @@ describe("models: task", () => {
code: 0,
result: expected,
})
- generator.next()
const end = generator.next()
expect(end.value).toBe(expected)
expect(end.done).toBe(true)
})
- it("effects: createTrainTask", () => {
- const saga = task.effects.createTrainTask
+
+ it("effects: filter", () => {
+ const saga = task.effects.filter
+ const creator = {
+ type: "filter",
+ payload: {},
+ }
+ const expected = "ok"
+
+ const generator = saga(creator, { put, call })
+ const start = generator.next()
+ generator.next({
+ code: 0,
+ result: expected,
+ })
+ const end = generator.next()
+
+ expect(end.value).toBe(expected)
+ expect(end.done).toBe(true)
+ })
+
+ it("effects: train", () => {
+ const saga = task.effects.train
const creator = {
- type: "createTrainTask",
+ type: "train",
payload: {},
}
const expected = "ok"
@@ -76,16 +118,15 @@ describe("models: task", () => {
code: 0,
result: expected,
})
- generator.next()
const end = generator.next()
expect(end.value).toBe(expected)
expect(end.done).toBe(true)
})
- it("effects: createMiningTask", () => {
- const saga = task.effects.createMiningTask
+ it("effects: mine", () => {
+ const saga = task.effects.mine
const creator = {
- type: "createMiningTask",
+ type: "mine",
payload: {},
}
const expected = "ok"
@@ -96,7 +137,6 @@ describe("models: task", () => {
code: 0,
result: expected,
})
- generator.next()
const end = generator.next()
expect(end.value).toBe(expected)
@@ -161,10 +201,10 @@ describe("models: task", () => {
expect(end.done).toBe(true)
})
- it("effects: createLabelTask", () => {
- const saga = task.effects.createLabelTask
+ it("effects: label", () => {
+ const saga = task.effects.label
const creator = {
- type: "createLabelTask",
+ type: "label",
payload: {},
}
const expected = "ok"
@@ -176,7 +216,6 @@ describe("models: task", () => {
result: expected,
})
- generator.next()
const end = generator.next()
expect(end.value).toBe(expected)
@@ -332,57 +371,4 @@ describe("models: task", () => {
expect(updated).toEqual(tasks)
expect(end.done).toBe(true)
})
-
- it("effects: updateTaskState -> normal success -> progress", () => {
- const saga = task.effects.updateTaskState
- const ts = { id: 34, hash: 'hash1', state: 2, progress: 20 }
- const creator = {
- type: "updateTaskState",
- payload: { hash1: { id: 34, state: 2, percent: 0.45 }, hash3: { id: 36, state: 3, percent: 1 } },
- }
- const expected = { id: 34, hash: 'hash1', state: 2, progress: 45 }
-
- const generator = saga(creator, { put, call, select })
- generator.next()
- const d = generator.next(ts)
- const end = generator.next()
- const updated = d.value.payload.action.payload
-
- expect(updated).toEqual(expected)
- expect(end.done).toBe(true)
- })
- it("effects: updateTaskState -> normal success -> progress to done", () => {
- const saga = task.effects.updateTaskState
- const ts = { id: 34, hash: 'hash1', state: 2, progress: 20 }
- const creator = {
- type: "updateTaskState",
- payload: { hash1: { id: 34, state: 3, percent: 1 }, hash3: { id: 36, state: 3, percent: 1 } },
- }
- const expected = { id: 34, hash: 'hash1', state: 3, progress: 100, forceUpdate: true }
-
- const generator = saga(creator, { put, call, select })
- generator.next()
- const d = generator.next(ts)
- const end = generator.next()
- const updated = d.value.payload.action.payload
-
- expect(updated).toEqual(expected)
- expect(end.done).toBe(true)
- })
- it("effects: updateTaskState -> empty success", () => {
- const saga = task.effects.updateTaskState
- const ts = { id: 34, hash: 'hash1', state: 2, progress: 20 }
- const creator = {
- type: "updateTaskState",
- }
-
- const generator = saga(creator, { put, call, select })
- generator.next()
- const d = generator.next(ts)
- const end = generator.next()
- const updated = d.value.payload.action.payload
-
- expect(updated).toEqual(ts)
- expect(end.done).toBe(true)
- })
})
diff --git a/ymir/web/src/models/common.js b/ymir/web/src/models/common.js
index a2137521ec..c481a1f8d4 100644
--- a/ymir/web/src/models/common.js
+++ b/ymir/web/src/models/common.js
@@ -3,10 +3,12 @@ import {
getStats,
getSysInfo,
} from "@/services/common"
+import { actions, updateResultByTask, ResultStates } from '@/constants/common'
export default {
namespace: "common",
state: {
+ loading: true,
},
effects: {
*getHistory({ payload }, { call }) {
@@ -27,7 +29,19 @@ export default {
return result
}
},
+ *setLoading({ payload }, { put }) {
+ yield put({
+ type: 'SET_LOADING',
+ payload,
+ })
+ },
},
reducers: {
+ SET_LOADING (state, { payload }) {
+ return {
+ ...state,
+ loading: payload,
+ }
+ }
},
}
diff --git a/ymir/web/src/models/dataset.js b/ymir/web/src/models/dataset.js
index 35cd290993..e9423299c1 100644
--- a/ymir/web/src/models/dataset.js
+++ b/ymir/web/src/models/dataset.js
@@ -1,15 +1,14 @@
import {
- getDatasetGroups, getDatasetByGroup, queryDatasets, getDataset, batchDatasets, evaluate,
+ getDatasetGroups, getDatasetByGroup, queryDatasets, getDataset, batchDatasets, evaluate, analysis,
getAssetsOfDataset, getAsset, batchAct, delDataset, delDatasetGroup, createDataset, updateDataset, getInternalDataset,
+ getNegativeKeywords,
} from "@/services/dataset"
-import { getStats } from "../services/common"
-import { transferDatasetGroup, transferDataset, states } from '@/constants/dataset'
-import { actions, updateResultState } from '@/constants/common'
+import { transferDatasetGroup, transferDataset, transferDatasetAnalysis, transferAsset, transferAnnotationsCount } from '@/constants/dataset'
+import { actions, updateResultState, updateResultByTask, ResultStates } from '@/constants/common'
import { deepClone } from '@/utils/object'
+import { checkDuplication } from "../services/dataset"
-let loading = false
-
-const initQuery = { name: "", type: "", time: 0, offset: 0, limit: 20 }
+const initQuery = { name: "", type: "", time: 0, current: 1, offset: 0, limit: 20 }
const initState = {
query: { ...initQuery },
@@ -39,22 +38,46 @@ export default {
return payload
}
},
+ *batchLocalDatasets({ payload }, { call, put }) {
+ const { pid, ids, ck } = payload
+ const cache = yield put.resolve({
+ type: 'getLocalDatasets',
+ payload: ids,
+ })
+ if (ids.length === cache.length) {
+ return cache
+ }
+ const fetchIds = ids.filter(id => cache.every(ds => ds.id !== id))
+ const remoteDatasets = yield put.resolve({
+ type: 'batchDatasets',
+ payload: { pid, ids: fetchIds, ck }
+ })
+ return [...cache, ...(remoteDatasets || [])]
+ },
*batchDatasets({ payload }, { call, put }) {
- const { code, result } = yield call(batchDatasets, payload)
+ const { pid, ids, ck } = payload
+ if (!ids?.length) {
+ return []
+ }
+ const { code, result } = yield call(batchDatasets, pid, ids, ck)
if (code === 0) {
const datasets = result.map(ds => transferDataset(ds))
+ yield put({
+ type: 'updateLocalDatasets',
+ payload: datasets,
+ })
return datasets || []
}
},
*getDataset({ payload }, { call, put, select }) {
- const { id, force } = payload
+ const { id, verbose, force } = payload
if (!force) {
const dataset = yield select(state => state.dataset.dataset[id])
if (dataset) {
return dataset
}
}
- const { code, result } = yield call(getDataset, id)
+ const { code, result } = yield call(getDataset, id, verbose)
if (code === 0) {
const dataset = transferDataset(result)
@@ -107,47 +130,52 @@ export default {
})
},
*queryAllDatasets({ payload }, { select, call, put }) {
- if (loading) {
- return
- }
- loading = true
+ const loading = yield select(({ loading }) => {
+ return loading.effects['dataset/queryDatasets']
+ })
const { pid, force } = payload
if (!force) {
const dssCache = yield select(state => state.dataset.allDatasets)
if (dssCache.length) {
- loading = false
return dssCache
}
}
- const dss = yield put.resolve({ type: 'queryDatasets', payload: { project_id: pid, state: states.VALID, limit: 10000 } })
+ if (loading) {
+ return
+ }
+ const dss = yield put.resolve({ type: 'queryDatasets', payload: { project_id: pid, state: ResultStates.VALID, limit: 10000 } })
if (dss) {
yield put({
type: "UPDATE_ALL_DATASETS",
payload: dss.items,
})
- loading = false
return dss.items
}
- loading = false
},
*getAssetsOfDataset({ payload }, { call, put }) {
+ const { datasetKeywords } = payload
const { code, result } = yield call(getAssetsOfDataset, payload)
if (code === 0) {
+ const { items, total } = result
+ const keywords = [...new Set(items.map(item => item.keywords).flat())]
+ const assets = { items: items.map(asset => transferAsset(asset, datasetKeywords || keywords)), total }
+
yield put({
type: "UPDATE_ASSETS",
- payload: result,
+ payload: assets,
})
- return result
+ return assets
}
},
*getAsset({ payload }, { call, put }) {
const { code, result } = yield call(getAsset, payload.id, payload.hash)
if (code === 0) {
+ const asset = transferAsset(result)
yield put({
type: "UPDATE_ASSET",
- payload: result,
+ payload: asset,
})
- return result
+ return asset
}
},
*delDataset({ payload }, { call, put }) {
@@ -182,6 +210,7 @@ export default {
*createDataset({ payload }, { call, put }) {
const { code, result } = yield call(createDataset, payload)
if (code === 0) {
+ // yield put.resolve({ type: 'clearCache' })
return result
}
},
@@ -221,40 +250,43 @@ export default {
})
return { ...versions }
},
- *updateDatasetState({ payload }, { put, select }) {
- const datasetCache = yield select(state => state.dataset.dataset)
- const tasks = payload || {}
- Object.keys(datasetCache).forEach(did => {
- const dataset = datasetCache[did]
- const updatedDataset = updateResultState(dataset, tasks)
- datasetCache[did] = updatedDataset ? updatedDataset : dataset
- })
-
- yield put({
- type: 'UPDATE_ALL_DATASET',
- payload: { ...datasetCache },
- })
- },
- *getHotDatasets({ payload }, { call, put }) {
- const { code, result } = yield call(getStats, { ...payload, q: 'ds' })
- let datasets = []
- if (code === 0) {
- const refs = {}
- const ids = result.map(item => {
- refs[item[0]] = item[1]
- return item[0]
+ *updateAllDatasets({ payload: tasks = {} }, { put, select }) {
+ const all = yield select(state => state.dataset.allDatasets)
+ const newDatasets = Object.values(tasks)
+ .filter(task => task.result_state === ResultStates.VALID)
+ .map(task => ({ id: task?.result_dataset?.id, needReload: true }))
+ const pid = yield select(({ project }) => project.current?.id)
+ if (newDatasets.length) {
+ yield put({
+ type: 'queryAllDatasets',
+ payload: { pid, force: true },
})
- if (ids.length) {
- const datasetsObj = yield put.resolve({ type: 'batchDatasets', payload: ids })
- if (datasetsObj) {
- datasets = datasetsObj.map(dataset => {
- dataset.count = refs[dataset.id]
- return dataset
+ }
+ },
+ *updateDatasetState({ payload }, { put, select }) {
+ const caches = yield select(state => state.dataset.dataset)
+ const tasks = Object.values(payload || {})
+ for (let index = 0; index < tasks.length; index++) {
+ const task = tasks[index]
+ const dataset = caches[task?.result_dataset?.id]
+ if (!dataset) {
+ continue
+ }
+ const updated = updateResultByTask(dataset, task)
+ if (updated?.id) {
+ if (updated.needReload) {
+ yield put({
+ type: 'getDataset',
+ payload: { id: updated.id, force: true }
+ })
+ } else {
+ yield put({
+ type: 'UPDATE_DATASET',
+ payload: { id: updated.id, dataset: { ...updated } },
})
}
}
}
- return datasets
},
*updateQuery({ payload = {} }, { put, select }) {
const query = yield select(({ task }) => task.query)
@@ -276,12 +308,80 @@ export default {
*clearCache({ }, { put }) {
yield put({ type: 'CLEAR_ALL', })
},
- *compare({ payload }, { call, put }) {
+ *evaluate({ payload }, { call, put }) {
const { code, result } = yield call(evaluate, payload)
if (code === 0) {
return result
}
},
+ *analysis({ payload }, { call, put }) {
+ const { pid, datasets } = payload
+ const { code, result } = yield call(analysis, pid, datasets)
+ if (code === 0) {
+ return result.map(item => transferDatasetAnalysis(item))
+ }
+ },
+ *checkDuplication({ payload }, { call, put, select }) {
+ const { trainSet, validationSet } = payload
+ const pid = yield select(({ project }) => project.current?.id)
+ const { code, result } = yield call(checkDuplication, pid, trainSet, validationSet)
+ if (code === 0) {
+ return result
+ }
+ },
+ *update({ payload }, { put, select }) {
+ const ds = transferDataset(payload)
+ if (!ds.id) {
+ return
+ }
+ const { versions } = yield select(({ dataset }) => dataset)
+ // update versions
+ const target = versions[ds.groupId] || []
+ yield put({
+ type: 'UPDATE_VERSIONS', payload: {
+ id: ds.groupId,
+ versions: [ds, ...target],
+ }
+ })
+ // update dataset
+ yield put({
+ type: 'UPDATE_DATASET', payload: {
+ id: ds.id,
+ dataset: ds,
+ }
+ })
+ },
+ *getNegativeKeywords({ payload }, { put, call, select }) {
+ const { code, result } = yield call(getNegativeKeywords, { ...payload })
+ if (code === 0) {
+ const { gt, pred, total_assets_count } = result
+ const getStats = (o = {}) => transferAnnotationsCount(o.keywords, o.negative_assets_count, total_assets_count)
+ return {
+ pred: getStats(pred),
+ gt: getStats(gt),
+ }
+ }
+ },
+ *getCK({ payload }, { select, put }) {
+ const { ids = [], pid } = payload
+ const datasets = yield put.resolve({ type: 'batchDatasets', payload: { pid, ids, ck: true } })
+ return datasets || []
+ },
+ *updateLocalDatasets({ payload: datasets }, { put }) {
+ for (let i = 0; i < datasets.length; i++) {
+ const dataset = datasets[i]
+ if (dataset?.id) {
+ yield put({
+ type: "UPDATE_DATASET",
+ payload: { id: dataset.id, dataset },
+ })
+ }
+ }
+ },
+ *getLocalDatasets({ payload: ids = [] }, { put, select }) {
+ const datasets = yield select(({ dataset }) => dataset.dataset)
+ return ids.map((id) => datasets[id]).filter(d => d)
+ },
},
reducers: {
UPDATE_DATASETS(state, { payload }) {
diff --git a/ymir/web/src/models/image.js b/ymir/web/src/models/image.js
index 219ea51c88..46d5cd2071 100644
--- a/ymir/web/src/models/image.js
+++ b/ymir/web/src/models/image.js
@@ -8,13 +8,7 @@ import {
relateImage,
getShareImages,
} from "@/services/image"
-
-function transferImage(image = {}) {
- return {
- ...image,
- functions: (image.configs || []).map(config => config.type),
- }
-}
+import { transferImage } from '@/constants/image'
export default {
namespace: "image",
diff --git a/ymir/web/src/models/iteration.js b/ymir/web/src/models/iteration.js
index bb7b562524..eb80bf0018 100644
--- a/ymir/web/src/models/iteration.js
+++ b/ymir/web/src/models/iteration.js
@@ -3,11 +3,11 @@ import {
getIteration,
createIteration,
updateIteration,
+ getMiningStats,
} from "@/services/iteration"
-import { Stages, transferIteration } from "@/constants/project"
+import { Stages, transferIteration, transferMiningStats } from "@/constants/iteration"
import { updateResultState } from '@/constants/common'
-
const initQuery = {
name: "",
offset: 0,
@@ -21,6 +21,8 @@ export default {
iterations: {},
iteration: {},
currentStageResult: {},
+ prepareStagesResult: {},
+ actionPanelExpand: true,
},
effects: {
*getIterations({ payload }, { call, put }) {
@@ -29,86 +31,116 @@ export default {
if (code === 0) {
let iterations = result.map((iteration) => transferIteration(iteration))
if (more && iterations.length) {
- const datasetIds = [...new Set(iterations.map(i => [
- i.miningSet,
- i.miningResult,
- i.labelSet,
- i.trainUpdateSet,
- i.testSet,
- ]).flat())].filter(id => id)
- const modelIds = [...new Set(iterations.map(i => i.model))].filter(id => id)
- let datasets = []
- let models = []
- if (datasetIds?.length) {
- datasets = yield put.resolve({
- type: 'dataset/batchDatasets',
- payload: datasetIds,
- })
- }
- if (modelIds?.length) {
- models = yield put.resolve({
- type: 'model/batchModels',
- payload: modelIds,
- })
- }
- iterations = iterations.map(i => {
- const ds = id => datasets.find(d => d.id === id)
- return {
- ...i,
- trainUpdateDataset: ds(i.trainUpdateSet),
- miningDataset: ds(i.miningSet),
- miningResultDataset: ds(i.miningResult),
- labelDataset: ds(i.labelSet),
- testDataset: ds(i.testSet),
- trainingModel: models.find(m => m.id === i.model),
- }
+ yield put.resolve({
+ type: 'moreIterationsInfo',
+ payload: { iterations, id },
})
}
yield put({
type: "UPDATE_ITERATIONS",
payload: { id, iterations },
})
+ // cache single iteration
+ yield put({
+ type: 'updateLocalIterations',
+ payload: iterations,
+ })
return iterations
}
},
- *getIteration({ payload }, { call, put }) {
+ *getIteration({ payload }, { call, put, select }) {
+ const { pid, id, more } = payload
+ let iteration = null
+ // try get iteration from cache
+ const cache = yield select(({ iteration }) => iteration.iteration[id])
+ if (cache) {
+ iteration = cache
+ } else {
+ iteration = yield put.resolve({
+ type: 'getRemoteIteration',
+ payload: { pid, id }
+ })
+ }
+ if (iteration && more) {
+ yield put.resolve({
+ type: 'moreIterationsInfo',
+ payload: { iterations: [iteration], id: pid },
+ })
+ }
+ yield put({
+ type: "UPDATE_ITERATION",
+ payload: iteration,
+ })
+ return iteration
+ },
+ *getRemoteIteration({ payload }, { call }) {
const { pid, id } = payload
const { code, result } = yield call(getIteration, pid, id)
if (code === 0) {
- const iteration = transferIteration(result)
- yield put({
- type: "UPDATE_ITERATION",
- payload: iteration,
- })
- return iteration
+ return transferIteration(result)
}
},
- *getIterationStagesResult({ payload }, { put }) {
- const iteration = payload
-
- const fields = ['miningSet', 'miningResult', 'labelSet', 'trainUpdateSet']
- const datasetIds = fields.map(field => iteration[field]).filter(id => id)
- const modelId = iteration.model
+ *moreIterationsInfo({ payload: { iterations, id } }, { put }) {
+ if (!iterations.length) {
+ return
+ }
+ const datasetIds = [...new Set(iterations.map(i => [
+ i.wholeMiningSet,
+ i.miningSet,
+ i.miningResult,
+ i.labelSet,
+ i.trainUpdateSet,
+ i.testSet,
+ ]).flat())].filter(id => id)
+ const modelIds = [...new Set(iterations.map(i => i.model))].filter(id => id)
let datasets = []
- let model = []
+ let models = []
if (datasetIds?.length) {
datasets = yield put.resolve({
- type: 'dataset/batchDatasets',
- payload: datasetIds,
+ type: 'dataset/batchLocalDatasets',
+ payload: { pid: id, ids: datasetIds },
})
}
- if (modelId) {
- model = yield put.resolve({
- type: 'model/getModel',
- payload: { id: modelId },
+ if (modelIds?.length) {
+ models = yield put.resolve({
+ type: 'model/batchLocalModels',
+ payload: modelIds,
})
}
- const ds = id => datasets.find(d => d.id === id)
- return {
- ...iteration,
- ...fields.reduce((prev, field) => ({ ...prev, [`i${field}`]: ds(iteration[field]) }), {}),
- imodel: model,
+ },
+ *getMiningStats({ payload }, { call, put }) {
+ const { pid, id } = payload
+ const { code, result } = yield call(getMiningStats, pid, id)
+ if (code === 0) {
+ return transferMiningStats(result)
+ }
+ },
+ *getPrepareStagesResult({ payload }, { put }) {
+
+ const project = yield put.resolve({
+ type: 'project/getProject',
+ payload,
+ })
+
+ if (project.candidateTrainSet) {
+ yield put.resolve({
+ type: 'dataset/getDataset',
+ payload: { id: project.candidateTrainSet, }
+ })
+ }
+
+ yield put.resolve({
+ type: 'dataset/updateLocalDatasets',
+ payload: [project.testSet, project.miningSet],
+ })
+
+ if (project.model) {
+ yield put.resolve({
+ type: 'model/getModel',
+ payload: { id: project.model, }
+ })
}
+ return true
},
*setCurrentStageResult({ payload }, { call, put }) {
const result = payload
@@ -117,17 +149,38 @@ export default {
return result
}
},
- *createIteration({ payload }, { call, put }) {
+ *createIteration({ payload }, { call, put, select }) {
+ const { projectId } = payload
const { code, result } = yield call(createIteration, payload)
if (code === 0) {
- return result
+ const iteration = transferIteration(result)
+ yield put({
+ type: 'updateLocalIterations',
+ payload: [iteration],
+ })
+ const iterations = yield select(({ iteration }) => iteration.iterations[projectId])
+ yield put({
+ type: 'UPDATE_ITERATIONS',
+ payload: { id: projectId, iterations: [...iterations, iteration] },
+ })
+ return iteration
}
},
- *updateIteration({ payload }, { call, put }) {
+ *updateIteration({ payload }, { call, put, select }) {
const { id, ...params } = payload
const { code, result } = yield call(updateIteration, id, params)
if (code === 0) {
- return transferIteration(result)
+ const iteration = transferIteration(result)
+ yield put({
+ type: 'updateLocalIterations',
+ payload: [{ ...iteration, needReload: true }],
+ })
+ const iterations = yield select(({ iteration: it }) => it.iterations[iteration.projectId])
+ yield put({
+ type: 'UPDATE_ITERATIONS',
+ payload: { id: iteration.projectId, iterations: iterations.map(it => it.id === iteration.id ? iteration : it) },
+ })
+ return iteration
}
},
*getStageResult({ payload }, { call, put }) {
@@ -138,23 +191,41 @@ export default {
type,
payload: { id, force },
})
- if (result) {
- yield put({ type: 'UPDATE_CURRENT_STAGE_RESULT', payload: result })
- return result
- }
+ return result
},
- *updateCurrentStageResult({ payload }, { put, select }) {
- const result = yield select(state => state.iteration.currentStageResult)
- const tasks = payload || {}
- const updated = updateResultState(result, tasks)
-
- if (updated) {
+ *updateLocalIterations({ payload: iterations = [] }, { put, select }) {
+ for (let i = 0; i < iterations.length; i++) {
+ const iteration = iterations[i];
yield put({
- type: 'UPDATE_CURRENT_STAGE_RESULT',
- payload: { ...updated },
+ type: 'UPDATE_ITERATION',
+ payload: iteration,
})
}
},
+ *updateIterationCache({ payload: tasks = {} }, { put, select }) {
+ // const tasks = payload || {}
+ const iteration = yield select(state => state.iteration.iteration)
+ const updateItertion = Object.keys(iteration).reduce((prev, key) => {
+ let item = iteration[key]
+ if (item.id) {
+ item = updateResultState(item, tasks)
+ }
+ return {
+ ...prev,
+ [key]: item,
+ }
+ }, {})
+ yield put({
+ type: 'UPDATE_ITERATION',
+ payload: updateItertion,
+ })
+ },
+ *toggleActionPanel({ payload }, { call, put, select }) {
+ yield put.resolve({
+ type: 'UPDATE_ACTIONPANELEXPAND',
+ payload,
+ })
+ },
},
reducers: {
UPDATE_ITERATIONS(state, { payload }) {
@@ -168,9 +239,13 @@ export default {
},
UPDATE_ITERATION(state, { payload }) {
const iteration = payload
+ const cache = state.iteration
return {
...state,
- iteration,
+ iteration: {
+ ...cache,
+ [iteration.id]: iteration,
+ },
}
},
UPDATE_CURRENT_STAGE_RESULT(state, { payload }) {
@@ -179,5 +254,21 @@ export default {
currentStageResult: payload,
}
},
+ UPDATE_PREPARE_STAGES_RESULT(state, { payload }) {
+ const { pid, results } = payload
+ return {
+ ...state,
+ prepareStagesResult: {
+ ...state.prepareStagesResult,
+ [pid]: results,
+ },
+ }
+ },
+ UPDATE_ACTIONPANELEXPAND(state, { payload }) {
+ return {
+ ...state,
+ actionPanelExpand: payload,
+ }
+ }
},
}
diff --git a/ymir/web/src/models/keyword.js b/ymir/web/src/models/keyword.js
index 645bb2de1e..b8b0520585 100644
--- a/ymir/web/src/models/keyword.js
+++ b/ymir/web/src/models/keyword.js
@@ -3,6 +3,7 @@ import {
updateKeyword,
updateKeywords,
getRecommendKeywords,
+ checkDuplication,
} from "@/services/keyword"
export default {
@@ -31,6 +32,13 @@ export default {
return result
}
},
+ *checkDuplication({ payload: keywords }, { call, put }) {
+ const { code, result } = yield call(checkDuplication, keywords)
+ if (code === 0) {
+ const newer = keywords.filter(kw => !result.includes(kw))
+ return {dup: result, newer }
+ }
+ },
*updateKeyword({ payload }, { call, put }) {
const { code, result } = yield call(updateKeyword, payload)
if (code === 0) {
@@ -38,9 +46,9 @@ export default {
}
},
*getRecommendKeywords({ payload }, { call, put }) {
- const data = yield call(getRecommendKeywords, payload)
- if (data.code === 0) {
- return data.result.map(item => item[0])
+ const { code, result } = yield call(getRecommendKeywords, payload)
+ if (code === 0) {
+ return result.map(item => item.legend)
}
}
},
diff --git a/ymir/web/src/models/model.js b/ymir/web/src/models/model.js
index a8c487ab2a..25a28f912e 100644
--- a/ymir/web/src/models/model.js
+++ b/ymir/web/src/models/model.js
@@ -10,15 +10,19 @@ import {
importModel,
updateModel,
verify,
+ setRecommendStage,
+ batchModelStages,
} from "@/services/model"
import { getStats } from "../services/common"
-import { transferModelGroup, transferModel, getModelStateFromTask, states, } from '@/constants/model'
-import { actions, updateResultState } from '@/constants/common'
+import { transferModelGroup, transferModel, getModelStateFromTask, states, transferStage, } from '@/constants/model'
+import { transferAnnotation } from '@/constants/dataset'
+import { actions, updateResultState, updateResultByTask } from '@/constants/common'
import { deepClone } from '@/utils/object'
const initQuery = {
name: "",
time: 0,
+ current: 1,
offset: 0,
limit: 20,
}
@@ -94,10 +98,29 @@ export default {
return dss.items
}
},
+ *batchLocalModels({ payload: ids = [] }, { call, put }) {
+ const cache = yield put.resolve({
+ type: 'getLocalModels',
+ payload: ids,
+ })
+ if (ids.length === cache.length) {
+ return cache
+ }
+ const fetchIds = ids.filter(id => cache.every(ds => ds.id !== id))
+ const remoteModels = yield put.resolve({
+ type: 'batchModels',
+ payload: fetchIds
+ })
+ return [...cache, ...remoteModels]
+ },
*batchModels({ payload }, { call, put }) {
const { code, result } = yield call(batchModels, payload)
if (code === 0) {
const models = result.map(model => transferModel(model))
+ yield put({
+ type: 'updateLocalModels',
+ payload: models,
+ })
return models
}
},
@@ -170,40 +193,73 @@ export default {
return result
}
},
+ *setRecommendStage({ payload }, { call, put }) {
+ const { model, stage } = payload
+ const { code, result } = yield call(setRecommendStage, model, stage)
+ if (code === 0) {
+ return transferModel(result)
+ }
+ },
*verify({ payload }, { call }) {
- const { id, urls, image, config } = payload
- const { code, result } = yield call(verify, id, urls, image, config)
+ const { code, result } = yield call(verify, payload)
if (code === 0) {
- return result
+ return result.annotations[0]?.detection?.map(transferAnnotation)
+ }
+ },
+ *batchModelStages({ payload }, { call, put }) {
+ const { code, result } = yield call(batchModelStages, payload)
+ if (code === 0) {
+ const stages = result.map(stage => transferStage(stage))
+ return stages || []
}
},
*updateModelsStates({ payload }, { put, select }) {
const versions = yield select(state => state.model.versions)
const tasks = payload || {}
+ const all = yield select(({ model }) => model.allModels)
+ const newModels = []
Object.keys(versions).forEach(gid => {
const models = versions[gid]
const updatedModels = models.map(model => {
const updatedModel = updateResultState(model, tasks)
+ newModels.push(updatedModel)
return updatedModel ? { ...updatedModel } : model
})
versions[gid] = updatedModels
})
+ const validNewModels = newModels.filter(model => model?.needReload)
+ yield put({
+ type: 'UPDATE_ALL_MODELS',
+ payload: [...validNewModels, ...all],
+ })
yield put({
type: 'UPDATE_ALL_VERSIONS',
payload: { ...versions },
})
},
*updateModelState({ payload }, { put, select }) {
- const models = yield select(state => state.model.model)
- const tasks = payload || {}
- const id = Object.keys(models).find(id => models[id])
- const updatedModel = updateResultState(models[id], tasks)
-
- if (updatedModel) {
- yield put({
- type: 'UPDATE_MODEL',
- payload: { id: updatedModel.id, model: { ...updatedModel } },
- })
+ const caches = yield select(state => state.model.model)
+ const tasks = Object.values(payload || {})
+ for (let index = 0; index < tasks.length; index++) {
+ const task = tasks[index]
+ const model = caches[task?.result_model?.id]
+ if (!model) {
+ continue
+ }
+ const updated = updateResultByTask(model, task)
+ if (updated) {
+ if (updated.needReload) {
+ yield put({
+ type: 'getModel',
+ payload: { id: updated.id, force: true }
+ })
+ } else {
+ yield put({
+ type: 'UPDATE_MODEL',
+ payload: { id: updated.id, model: { ...updated } },
+ })
+ }
+ }
}
},
*getModelsByMap({ payload }, { call, put }) {
@@ -249,6 +305,19 @@ export default {
*clearCache({ }, { put }) {
yield put({ type: 'CLEAR_ALL', })
},
+ *updateLocalModels({ payload: models = [] }, { put }) {
+ for (let i = 0; i < models.length; i++) {
+ const model = models[i]
+ yield put({
+ type: "UPDATE_MODEL",
+ payload: { id: model.id, model },
+ })
+ }
+ },
+ *getLocalModels({ payload: ids = [] }, { put, select }) {
+ const models = yield select(({ model }) => model.model)
+ return ids.map((id) => models[id]).filter(d => d)
+ },
},
reducers: {
UPDATE_MODELS(state, { payload }) {
diff --git a/ymir/web/src/models/project.js b/ymir/web/src/models/project.js
index 7aa0cd3052..72c0abadfc 100644
--- a/ymir/web/src/models/project.js
+++ b/ymir/web/src/models/project.js
@@ -12,6 +12,7 @@ import { deepClone } from '@/utils/object'
const initQuery = {
name: "",
+ current: 1,
offset: 0,
limit: 20,
}
@@ -22,6 +23,7 @@ const initState = {
total: 0,
},
projects: {},
+ current: null,
}
export default {
@@ -45,6 +47,10 @@ export default {
const cache = yield select(state => state.project.projects)
const cacheProject = cache[id]
if (cacheProject) {
+ yield put({
+ type: 'UPDATE_CURRENT',
+ payload: cacheProject,
+ })
return cacheProject
}
}
@@ -55,6 +61,10 @@ export default {
type: "UPDATE_PROJECTS",
payload: project,
})
+ yield put({
+ type: 'UPDATE_CURRENT',
+ payload: project,
+ })
return project
}
},
@@ -80,10 +90,12 @@ export default {
const { id, ...params } = payload
const { code, result } = yield call(updateProject, id, params)
if (code === 0) {
+ const project = transferProject(result)
yield put({
- type: 'clearCache'
+ type: "UPDATE_PROJECTS",
+ payload: project,
})
- return result
+ return project
}
},
*updateQuery({ payload = {} }, { put, select }) {
@@ -130,6 +142,18 @@ export default {
projects,
}
},
+ UPDATE_CURRENT(state, { payload }) {
+ return {
+ ...state,
+ current: payload,
+ }
+ },
+ UPDATE_PREPARETRAINSET(state, { payload }) {
+ return {
+ ...state,
+ prepareTrainSet: payload,
+ }
+ },
UPDATE_QUERY(state, { payload }) {
return {
...state,
diff --git a/ymir/web/src/models/socket.js b/ymir/web/src/models/socket.js
index 56139b14ba..2c462f9a35 100644
--- a/ymir/web/src/models/socket.js
+++ b/ymir/web/src/models/socket.js
@@ -1,13 +1,15 @@
import { getSocket } from '../services/socket'
const pageMaps = [
- { path: '/home/project/detail/\\d+', method: 'dataset/updateDatasets' },
- { path: '/home/project/detail/\\d+', method: 'dataset/updateDatasetState' },
- { path: '/home/project/detail/\\d+', method: 'model/updateModelState' },
- { path: '/home/project/detail/\\d+', method: 'iteration/updateCurrentStageResult' },
- { path: '/home/project/detail/\\d+', method: 'model/updateModelsStates' },
+ { path: '/home/project/\\d+/dataset', method: 'dataset/updateDatasets' },
+ { path: '/home/project/\\d+/model', method: 'model/updateModelsStates' },
{ path: '/home/project/\\d+/model/\\d+', method: 'model/updateModelState' },
{ path: '/home/project/\\d+/dataset/\\d+', method: 'dataset/updateDatasetState' },
+ { path: '/home/project/\\d+/iterations', method: 'dataset/updateAllDatasets' },
+ { path: '/home/project/\\d+/iterations', method: 'dataset/updateDatasetState' },
+ { path: '/home/project/\\d+/iterations', method: 'model/updateModelState' },
+ { path: '/home/project/\\d+/iterations', method: 'project/updateProjectTrainSet' },
+ { path: '/home/project/\\d+/iterations', method: 'iteration/updateIterationCache' },
]
export default {
diff --git a/ymir/web/src/models/task.js b/ymir/web/src/models/task.js
index f0f02a2a42..fd0b1ee1b6 100644
--- a/ymir/web/src/models/task.js
+++ b/ymir/web/src/models/task.js
@@ -4,13 +4,15 @@ import {
deleteTask,
updateTask,
stopTask,
- createFusionTask,
- createMiningTask,
- createTrainTask,
- createLabelTask,
- createInferenceTask,
+ fusion,
+ merge,
+ filter,
+ mine,
+ train,
+ label,
+ infer,
} from "@/services/task"
-import { isFinalState } from '@/constants/task'
+import { TASKTYPES, TASKSTATES, isFinalState } from '@/constants/task'
const initQuery = {
name: '',
@@ -42,6 +44,13 @@ export default {
return result
}
},
+ *queryInferTasks({ payload }, { call, put }) {
+ const params = { ...payload, type: TASKTYPES.INFERENCE, state: TASKSTATES.FINISH, limit: 1000 }
+ const result = yield put.resolve({ type: 'getTasks', payload: params })
+ if (result) {
+ return result.items
+ }
+ },
*getTask({ payload }, { call, put }) {
let { code, result } = yield call(getTask, payload)
if (code === 0) {
@@ -57,7 +66,7 @@ export default {
...excludeSets,
]
if (ids.length) {
- const datasets = yield put.resolve({ type: 'dataset/batchDatasets', payload: ids })
+ const datasets = yield put.resolve({ type: 'dataset/batchDatasets', payload: { pid: result?.project_id, ids } })
const findDs = (dss) => dss.map(sid => datasets.find(ds => ds.id === sid))
if (datasets && datasets.length) {
result['filterSets'] = findDs(filterSets)
@@ -107,63 +116,77 @@ export default {
return result
}
},
- *createFusionTask({ payload }, { call, put }) {
- let { code, result } = yield call(createFusionTask, payload)
+ *fusion({ payload }, { call, put }) {
+ let { code, result } = yield call(fusion, payload)
if (code === 0) {
yield put.resolve({
- type: 'dataset/clearCache'
- })
- yield put.resolve({
- type: 'project/clearCache'
+ type: 'dataset/update',
+ payload: result,
})
return result
}
},
- *createTrainTask({ payload }, { call, put }) {
- let { code, result } = yield call(createTrainTask, payload)
+ *merge({ payload }, { call, put }) {
+ let { code, result } = yield call(merge, payload)
if (code === 0) {
yield put.resolve({
- type: 'model/clearCache'
+ type: 'dataset/update',
+ payload: result,
})
+ return result
+ }
+ },
+ *filter({ payload }, { call, put }) {
+ let { code, result } = yield call(filter, payload)
+ if (code === 0) {
yield put.resolve({
- type: 'project/clearCache'
+ type: 'dataset/update',
+ payload: result,
})
return result
}
},
- *createMiningTask({ payload }, { call, put }) {
- let { code, result } = yield call(createMiningTask, payload)
+ *train({ payload }, { call, put }) {
+ let { code, result } = yield call(train, payload)
if (code === 0) {
yield put({
- type: 'dataset/clearCache'
+ type: 'model/getModel',
+ payload: { id: result?.result_model?.id, force: true }
})
+ return result
+ }
+ },
+ *mine({ payload }, { call, put }) {
+ let { code, result } = yield call(mine, payload)
+ if (code === 0) {
yield put({
- type: 'project/clearCache'
+ type: 'dataset/getDataset',
+ payload: { id: result?.result_dataset?.id, force: true }
})
return result
}
},
- *createLabelTask({ payload }, { call, put }) {
- let { code, result } = yield call(createLabelTask, payload)
+ *label({ payload }, { call, put }) {
+ let { code, result } = yield call(label, payload)
if (code === 0) {
- yield put.resolve({
- type: 'dataset/clearCache'
- })
- yield put.resolve({
- type: 'project/clearCache'
+ yield put({
+ type: 'dataset/getDataset',
+ payload: { id: result?.result_dataset?.id, force: true }
})
return result
}
},
- *createInferenceTask({ payload }, { call, put }) {
- let { code, result } = yield call(createInferenceTask, payload)
+ *infer({ payload }, { call, put }) {
+ let { code, result } = yield call(infer, payload)
if (code === 0) {
- yield put.resolve({
- type: 'dataset/clearCache'
- })
- yield put.resolve({
- type: 'project/clearCache'
- })
+ const ids = result.map(item => item?.result_dataset?.id).filter(i => i)
+ const pid = result[0]?.project_id
+ if (pid && ids?.length) {
+ yield put({
+ type: 'dataset/batchLocalDatasets',
+ payload: { ids, pid }
+ })
+ }
return result
}
},
@@ -186,22 +209,6 @@ export default {
payload: { items: result, total: tasks.total },
})
},
- *updateTaskState({ payload }, { put, select }) {
- const task = yield select(state => state.task.task)
- const updateList = payload || {}
- const updateItem = updateList[task.hash]
- if (updateItem) {
- task.state = updateItem.state
- task.progress = updateItem.percent * 100
- if (isFinalState(updateItem.state)) {
- task.forceUpdate = true
- }
- }
- yield put({
- type: 'UPDATE_TASK',
- payload: { ...task },
- })
- },
*updateQuery({ payload = {} }, { put, select }) {
const query = yield select(({ task }) => task.query)
yield put({
@@ -213,7 +220,7 @@ export default {
}
})
},
- *resetQuery({}, { put }) {
+ *resetQuery({ }, { put }) {
yield put({
type: 'UPDATE_QUERY',
payload: initQuery,
diff --git a/ymir/web/src/models/watchRoute.js b/ymir/web/src/models/watchRoute.js
index 400e495424..4d5e551cc3 100644
--- a/ymir/web/src/models/watchRoute.js
+++ b/ymir/web/src/models/watchRoute.js
@@ -7,7 +7,6 @@ const WatchRoute = {
},
effects: {
*updateRoute({ payload }, { put }) {
- // console.log('effect--updateRoute', payload)
put({
type: 'UPDATEROUTE',
payload,
@@ -16,7 +15,6 @@ const WatchRoute = {
},
reducers: {
UPDATEROUTE(state, { payload }) {
- // console.log('STATE,',state, payload)
return {
...state,
current: payload,
diff --git a/ymir/web/src/pages/algo/index.tsx b/ymir/web/src/pages/algo/index.tsx
new file mode 100644
index 0000000000..15d370bf25
--- /dev/null
+++ b/ymir/web/src/pages/algo/index.tsx
@@ -0,0 +1,83 @@
+import { getDeployUrl } from '@/constants/common'
+import usePostMessage from '@/hooks/usePostMessage'
+import { message } from 'antd'
+import { useEffect, useRef, useState } from 'react'
+import { getLocale, history, useLocation, useParams, useSelector } from 'umi'
+type Params = { [key: string]: any }
+
+const base = getDeployUrl()
+
+const pages: Params = {
+ public: { path: '/publicAlgorithm', action: 'pageInit' },
+ mine: { path: '/algorithmManagement', action: 'pageInit' },
+ device: { path: '/device', action: 'pageInit' },
+ support: { path: '/supportDeviceList', action: 'pageInit' },
+}
+
+const Algo = () => {
+ if (!base) {
+ return Algorithm Store is not READY
+ }
+ const { username: userName, id: userId } = useSelector((state: Params) => state.user)
+ const { module = 'public' } = useParams()
+ const location: Params = useLocation()
+ const iframe: { current: HTMLIFrameElement | null } = useRef(null)
+ const [url, setUrl] = useState(base)
+ const [post, recieved] = usePostMessage(base)
+ const [key, setKey] = useState(Math.random())
+
+ useEffect(() => {
+ if (!location.state?.reload) {
+ const r = Math.random()
+ setKey(r)
+ const self = window.location.origin
+ const lang = getLocale()
+ const url = `${base}${pages[module].path}?from=${self}&userId=${userId}&userName=${userName || ''}&lang=${lang}&r=${r}`
+ setUrl(url)
+ }
+ history.replace({ state: {} })
+ }, [module])
+
+ useEffect(() => {
+ if (!recieved) {
+ return
+ }
+ if (recieved.type === 'loaded') {
+ // send()
+ } else if (recieved.type === 'pageChanged') {
+ const page = Object.keys(pages).find(key => (recieved.data?.path || '').includes(pages[key].path))
+ if (page !== module) {
+ const mod = page === 'public' ? '' : `/${page}`
+ history.push(`/home/algo${mod}`, { reload: true })
+ }
+ }
+ }, [recieved])
+
+ function getPageParams(module: string) {
+ const lang = getLocale()
+ return {
+ module,
+ userId,
+ userName,
+ lang,
+ }
+ }
+
+ function send() {
+ if (iframe.current?.contentWindow) {
+ const params = getPageParams(module)
+ post(pages[module].action, params, iframe.current.contentWindow)
+ }
+ }
+
+ const iframeStyles = {
+ border: 'none',
+ width: '100%',
+ height: 'calc(100vh - 120px)',
+ }
+ return
+
+
+}
+
+export default Algo
diff --git a/ymir/web/src/pages/dataset/add.js b/ymir/web/src/pages/dataset/add.js
index 4caab0ad97..71c85df70f 100644
--- a/ymir/web/src/pages/dataset/add.js
+++ b/ymir/web/src/pages/dataset/add.js
@@ -1,21 +1,23 @@
import { useEffect, useState } from 'react'
-import { Button, Card, Form, Input, message, Radio, Row, Col, Select, Space, Tag } from 'antd'
-import { connect } from 'dva'
-import { Link, useParams, useHistory } from 'umi'
+import { Button, Card, Form, Input, message, Radio, Select, Space, Tag } from 'antd'
+import { useParams, useHistory, useLocation } from 'umi'
import { formLayout } from "@/config/antd"
import t from '@/utils/t'
-import Uploader from '@/components/form/uploader'
-import { randomNumber } from '@/utils/number'
+import useFetch from '@/hooks/useFetch'
+import useAddKeywords from '@/hooks/useAddKeywords'
+import { IMPORTSTRATEGY } from '@/constants/dataset'
+
import { urlValidator } from '@/components/form/validators'
-import s from './add.less'
import Breadcrumbs from '@/components/common/breadcrumb'
-import { TipsIcon } from '@/components/common/icons'
-import { getKeywords } from '../../services/keyword'
-import Tip from "@/components/form/tip"
-import ProjectDatasetSelect from '../../components/form/projectDatasetSelect'
-import useAddKeywords from '@/hooks/useAddKeywords'
+import Uploader from '@/components/form/uploader'
+import ProjectDatasetSelect from '@/components/form/projectDatasetSelect'
+import Desc from "@/components/form/desc"
+
+import s from './add.less'
import samplePic from '@/assets/sample.png'
+import DatasetName from '../../components/form/items/datasetName'
+import { FormatDetailModal } from './components/formatDetailModal'
const { Option } = Select
const { useForm } = Form
@@ -27,104 +29,140 @@ const TYPES = Object.freeze({
LOCAL: 4,
PATH: 5,
})
-
+const types = [
+ { id: TYPES.INTERNAL, label: 'internal' },
+ { id: TYPES.COPY, label: 'copy' },
+ { id: TYPES.NET, label: 'net' },
+ { id: TYPES.LOCAL, label: 'local' },
+ { id: TYPES.PATH, label: 'path' },
+]
+const strategies = [
+ { value: IMPORTSTRATEGY.UNKOWN_KEYWORDS_IGNORE, label: 'ignore', },
+ { value: IMPORTSTRATEGY.UNKOWN_KEYWORDS_AUTO_ADD, label: 'add', },
+ { value: IMPORTSTRATEGY.ALL_KEYWORDS_IGNORE, label: 'exclude', },
+]
const Add = (props) => {
const history = useHistory()
+ const { query } = useLocation()
const pageParams = useParams()
const pid = Number(pageParams.id)
- const { id } = history.location.query
- const types = [
- { id: TYPES.INTERNAL, label: t('dataset.add.types.internal') },
- { id: TYPES.COPY, label: t('dataset.add.types.copy') },
- { id: TYPES.NET, label: t('dataset.add.types.net') },
- { id: TYPES.LOCAL, label: t('dataset.add.types.local') },
- { id: TYPES.PATH, label: t('dataset.add.types.path') },
- ]
- const labelOptions = [
- { value: 0, label: t('dataset.add.label_strategy.include'), },
- { value: 1, label: t('dataset.add.label_strategy.exclude'), },
- ]
- const labelStrategyOptions = [
- { value: 0, label: t('dataset.add.label_strategy.ignore'), },
- { value: 1, label: t('dataset.add.label_strategy.add'), },
- { value: 2, label: t('dataset.add.label_strategy.stop'), },
- ]
+ const { id, from, stepKey } = query
+ const iterationContext = from === 'iteration'
+
const [form] = useForm()
const [currentType, setCurrentType] = useState(TYPES.INTERNAL)
- const [publicDataset, setPublicDataset] = useState([])
- const [selected, setSelected] = useState([])
- const [fileToken, setFileToken] = useState('')
+ const [file, setFile] = useState('')
const [selectedDataset, setSelectedDataset] = useState(id ? Number(id) : null)
- const [showLabelStrategy, setShowLS] = useState(true)
- const [strategy, setStrategy] = useState(2)
- const [kStrategy, setKStrategy] = useState(0)
const [newKeywords, setNewKeywords] = useState([])
- const [currentKeywords, setCurrentKeywords] = useState([])
- const [{ newer }, checkKeywords] = useAddKeywords(true)
+ const [strategyOptions, setStrategyOptions] = useState([])
+ const [ignoredKeywords, setIgnoredKeywords] = useState([])
+ const [{ newer }, checkKeywords] = useFetch('keyword/checkDuplication', { newer: [] })
+ const [_, updateKeywords] = useAddKeywords()
+ const [addResult, newDataset] = useFetch('dataset/createDataset')
+ const [{ items: publicDatasets }, getPublicDatasets] = useFetch('dataset/getInternalDataset', { items: [] })
+ const [nameChangedByUser, setNameChangedByUser] = useState(false)
+ const [defaultName, setDefaultName] = useState('')
+ const netUrl = Form.useWatch('url', form)
+ const path = Form.useWatch('path', form)
+ const [formatDetailModal, setFormatDetailModal] = useState(false)
+ const [updateResult, updateProject] = useFetch('project/updateProject')
+ useEffect(() => {
+ form.setFieldsValue({ datasetId: null })
+ setDefaultName('')
+ }, [currentType])
useEffect(async () => {
- if (!publicDataset.length) {
- const result = await props.getInternalDataset()
- if (result) {
- setPublicDataset(result.items)
- }
- }
+ getPublicDatasets()
}, [])
useEffect(() => {
- const ds = publicDataset.find(set => set.id === selectedDataset)
- if (ds) {
- setNewKeywords(ds.keywords)
- }
+ const kws = getSelectedDatasetKeywords()
+ checkKeywords(kws)
}, [selectedDataset])
useEffect(() => {
- setStrategy(kStrategy === labelStrategyOptions[2].value ? 3 : 2)
- if (kStrategy === 1) {
- renderKeywords()
+ setNewKeywords(newer)
+ setIgnoredKeywords([])
+ }, [newer])
+
+ useEffect(() => {
+ const filename = (netUrl || '').replace(/^.+\/([^\/]+)\.zip$/, '$1')
+ setDefaultName(filename)
+ }, [netUrl])
+
+ useEffect(() => {
+ if (typeof path === 'undefined') {
+ return
}
- }, [kStrategy])
+ const matchfinalDir = path.match(/[^\/]+$/) || []
+ const finalDir = matchfinalDir[0]
+ setDefaultName(finalDir)
+ }, [path])
+
+ useEffect(() => addDefaultName(defaultName), [defaultName])
useEffect(() => {
- renderKeywords()
- }, [selectedDataset])
+ if (addResult) {
+ message.success(t('dataset.add.success.msg'))
+ if (iterationContext && stepKey) {
+ return updateProject({ id: pid, [stepKey]: addResult.id })
+ }
+ const group = addResult.dataset_group_id || ''
+ history.replace(`/home/project/${pid}/dataset#${group}`)
+ }
+ }, [addResult])
useEffect(() => {
- if (newer.length) {
- const unique = newer.map(k => ({ name: k, type: 0 }))
- setNewKeywords(unique)
- form.setFieldsValue({ new_keywords: unique })
+ if (updateResult) {
+ history.replace(`/home/project/${pid}/iterations`)
}
- }, [newer])
+ }, [updateResult])
+
+ useEffect(() => {
+ const opts = strategies.map(opt => ({ ...opt, label: t(`dataset.add.label_strategy.${opt.label}`) }))
+ setStrategyOptions(opts)
+ }, [currentType])
const typeChange = (type) => {
setCurrentType(type)
- form.setFieldsValue({ with_annotations: 0, k_strategy: 0, })
- setShowLS(true)
- setKStrategy(0)
+ form.setFieldsValue({
+ strategy: IMPORTSTRATEGY.UNKOWN_KEYWORDS_IGNORE,
+ })
}
const isType = (type) => {
return currentType === type
}
+ async function addNewKeywords() {
+ if (!newKeywords.length) {
+ return
+ }
+ const result = await updateKeywords(newKeywords)
+ return result || message.error(t('keyword.add.failure'))
+ }
+
async function submit(values) {
- if (currentType === TYPES.LOCAL && !fileToken) {
+ let updateKeywordResult = null
+ if (currentType === TYPES.LOCAL && !file) {
return message.error(t('dataset.add.local.file.empty'))
}
- const kws = form.getFieldValue('new_keywords')
- if (kws && kws.length && kStrategy === 1 && isType(TYPES.INTERNAL)) {
- const addKwParams = kws.filter(k => k.type === 0).map(k => ({ name: k.name }))
- const kResult = await props.updateKeywords({ keywords: addKwParams })
- if (!kResult) {
- return message.error(t('keyword.add.failure'))
+
+ if (newKeywords.length && isType(TYPES.INTERNAL)) {
+ updateKeywordResult = await addNewKeywords()
+ if (updateKeywordResult) {
+ addDataset({ ...values, strategy: IMPORTSTRATEGY.UNKOWN_KEYWORDS_IGNORE })
}
+ } else {
+ addDataset(values)
}
+ }
+
+ function addDataset(values) {
let params = {
...values,
- strategy,
projectId: pid,
url: (values.url || '').trim(),
}
@@ -132,8 +170,8 @@ const Add = (props) => {
params.datasetId = params.datasetId[1]
}
if (currentType === TYPES.LOCAL) {
- if (fileToken) {
- params.url = fileToken
+ if (file) {
+ params.url = file
} else {
return message.error(t('dataset.add.local.file.empty'))
}
@@ -141,52 +179,74 @@ const Add = (props) => {
if (isType(TYPES.PATH)) {
params.path = `/ymir-sharing/${params.path}`
}
- const result = await props.createDataset(params)
- if (result) {
- message.success(t('dataset.add.success.msg'))
- props.clearCache()
- history.push(`/home/project/detail/${pid}`)
- }
+ newDataset(params)
}
function onFinishFailed(err) {
console.log('finish failed: ', err)
}
- function onLabelChange({ target }) {
- setShowLS(target.value === labelOptions[0].value)
- setStrategy(target.value === labelOptions[0].value ? 2 : 1)
+ function onInternalDatasetChange(value, { dataset }) {
+ setDefaultName(`${dataset.name}`)
+ setSelectedDataset(value)
}
- function onStrategyChange({ target }) {
- setKStrategy(target.value)
+ function setFileDefaultName([file]) {
+ const filename = file.name.replace(/\.zip$/i, '')
+ setDefaultName(filename)
}
- async function renderKeywords() {
- const kws = getSelectedDatasetKeywords()
- await checkKeywords(kws)
+ function setCopyDefaultName(value, option) {
+ const label = value ? option[1]?.label : ''
+ const datasetname = label.replace(/\sV\d+\s\(assets: \d+\)/, '')
+ setDefaultName(datasetname)
}
- function onInternalDatasetChange(value) {
- setSelectedDataset(value)
+ function addDefaultName(name = '') {
+ if (!nameChangedByUser) {
+ form.setFieldsValue({ name })
+ }
}
- function renderSelectedKeywords() {
- const kws = getSelectedDatasetKeywords()
- return kws.length ? kws.map(key => {key} ) : t('common.empty.keywords')
+ function updateIgnoredKeywords(e, keywords, isRemove) {
+ e.preventDefault()
+ const add = old => [...old, ...keywords]
+ const remove = old => old.filter(selected => !keywords.includes(selected))
+ setIgnoredKeywords(isRemove ? remove : add)
+ setNewKeywords(isRemove ? add : remove)
+ }
+
+ function renderKeywords(keywords, isIgnoreKeywords) {
+ return keywords.length ? keywords.map(key =>
+ updateIgnoredKeywords(e, [key], isIgnoreKeywords)}
+ >
+ {key}
+
+ ) : t('common.empty.keywords')
}
function getSelectedDatasetKeywords() {
- const set = publicDataset.find(d => d.id === selectedDataset)
+ const set = publicDatasets.find(d => d.id === selectedDataset)
return set?.keywords || []
}
- function filterDataset() {
- return selected.length ? publicDataset.filter(dataset => {
- return dataset.keywords.some((key) => selected.indexOf(key) > -1)
- }) : publicDataset
+ function showFormatDetail() {
+ setFormatDetailModal(true)
}
+ const structureTip = t('dataset.add.form.tip.structure', {
+ br: ,
+ pic: ,
+ detail: {t('dataset.add.form.tip.format.detail')} ,
+ })
+
+ const renderTip = (type, params = {}) => t(`dataset.add.form.${type}.tip`, {
+ ...params,
+ br: ,
+ structure: structureTip,
+ })
+
return (
@@ -197,229 +257,135 @@ const Add = (props) => {
className={s.form}
{...formLayout}
form={form}
- // initialValues={initialValues}
onFinish={submit}
onFinishFailed={onFinishFailed}
- labelAlign={'left'}
- colon={false}
>
-
-
-
-
-
-
-
-
- typeChange(value)} defaultValue={TYPES.INTERNAL}>
- {types.map(type => (
- {type.label}
- ))}
-
-
-
+
setNameChangedByUser(true)
+ }} />
+
+ typeChange(value)} defaultValue={TYPES.INTERNAL}>
+ {types.map(type => (
+ {t(`dataset.add.types.${type.label}`)}
+ ))}
+
+
{isType(TYPES.INTERNAL) ? (
<>
-
-
- onInternalDatasetChange(value)}>
- {filterDataset().map(dataset => (
- {dataset.name} {dataset.versionName} (Total: {dataset.assetCount})
- ))}
-
-
-
+
+ ({
+ value: dataset.id,
+ dataset,
+ label: `${dataset.name} ${dataset.versionName} (Total: ${dataset.assetCount})`
+ }))}>
+
+
{selectedDataset ?
-
-
- {renderSelectedKeywords()}
-
-
+
+ {newKeywords.length ? <>
+
+ {t('dataset.add.internal.newkeywords.label')}
+ updateIgnoredKeywords(e, newKeywords, false)}>{t('dataset.add.internal.ignore.all')}
+
+ {renderKeywords(newKeywords)}
+ > : null}
+ {ignoredKeywords.length ? <>
+
+ {t('dataset.add.internal.ignorekeywords.label')}
+ updateIgnoredKeywords(e, ignoredKeywords, true)}>{t('dataset.add.internal.add.all')}
+
+ {renderKeywords(ignoredKeywords, true)}
+ > : null}
+
: null}
>
) : null}
{isType(TYPES.COPY) ? (
-
+
+
+
+ ) : null}
+ {!isType(TYPES.INTERNAL) ?
+
+ !isType(TYPES.COPY) || opt.value !== IMPORTSTRATEGY.UNKOWN_KEYWORDS_AUTO_ADD)} />
+ : null}
+ {isType(TYPES.NET) ? (
+
-
+
-
- ) : null}
-
-
- isType(TYPES.INTERNAL) ? option.value !== 1 : true)}
- onChange={onLabelChange}
- />
+ {renderTip('net')}
-
- {!isType(TYPES.COPY) ? <>
- {showLabelStrategy ?
-
-
- {t('dataset.add.form.newkw.tip')}
-
- isType(TYPES.INTERNAL) ? option.value !== 2 : option.value !== 1)}
- onChange={onStrategyChange} />
-
- {t('dataset.add.form.newkw.link')}
-
-
- : null}
-
-
-
- {newKeywords.length > 0 ?
-
- {(fields, { add, remove }) => (
-
-
- {fields.map((field, index) => (
-
-
- {newKeywords[field.name]?.name}
-
-
-
- {t('dataset.add.newkw.asname')}
- {/* {t('dataset.add.newkw.asalias')} */}
- {t('dataset.add.newkw.ignore')}
-
-
-
-
-
- ))}
-
-
- )}
-
- : t('dataset.add.newkeyword.empty')}
-
- > : null}
- {isType(TYPES.NET) ? (
-
-
-
-
-
- Sample: https://www.examples.com/pascal.zip
-
-
) : null}
{isType(TYPES.PATH) ? (
-
-
-
-
-
+
+
+
) : null}
{isType(TYPES.LOCAL) ? (
-
-
- { setFileToken(result) }}
- max={1024}
- onRemove={() => setFileToken('')}
- info={t('dataset.add.form.upload.tip', {
- br: ,
- sample: Sample.zip ,
- pic:
- })}
- >
-
-
- ) : null}
-
-
-
-
-
- {t('common.action.import')}
-
-
-
- history.goBack()}>
- {t('task.btn.back')}
-
-
-
+
+ { setFile(result); setFileDefaultName(files) }}
+ max={1024}
+ onRemove={() => setFile('')}
+ info={renderTip('upload', {
+ sample: Sample.zip ,
+ })}
+ >
-
+ ) : null}
+
+
+
+
+
+ {t('common.action.import')}
+
+
+
+ history.goBack()}>
+ {t('task.btn.back')}
+
+
+
+
+ setFormatDetailModal(false)} />
)
}
-
-const actions = (dispatch) => {
- return {
- getInternalDataset: (payload) => {
- return dispatch({
- type: 'dataset/getInternalDataset',
- payload,
- })
- },
- createDataset: (payload) => {
- return dispatch({
- type: 'dataset/createDataset',
- payload,
- })
- },
- clearCache() {
- return dispatch({ type: "dataset/clearCache", })
- },
- updateKeywords: (payload) => {
- return dispatch({
- type: 'keyword/updateKeywords',
- payload,
- })
- },
- }
-}
-
-export default connect(null, actions)(Add)
+export default Add
diff --git a/ymir/web/src/pages/dataset/add.less b/ymir/web/src/pages/dataset/add.less
index f5887a4659..74e7d06337 100644
--- a/ymir/web/src/pages/dataset/add.less
+++ b/ymir/web/src/pages/dataset/add.less
@@ -5,17 +5,17 @@
background: #fff;
display: flex;
flex-direction: column;
- height: calc(100vh - 180px);
+ height: calc(100vh - 186px);
:global(.ant-card-body) {
flex: 1;
overflow-y: auto;
}
}
.newkwTip {
- @color: rgb(79, 187, 187);
- background: fade(@primary-color, 10);
+ @color: rgb(24, 144, 255);
+ background: fade(rgb(44, 189, 233), 10);
border-radius: 2px;
- border: 1px solid fade(@primary-color, 50);
+ border: 1px solid rgb(24, 144, 255);
line-height: 25px;
padding: 6px 15px;
color: @color;
diff --git a/ymir/web/src/pages/dataset/analysis.js b/ymir/web/src/pages/dataset/analysis.js
new file mode 100644
index 0000000000..a7a6ae3bc2
--- /dev/null
+++ b/ymir/web/src/pages/dataset/analysis.js
@@ -0,0 +1,345 @@
+import React, { useEffect, useState } from "react"
+import { Button, Form, Row, Col, Table, Popover, Card, Radio } from "antd"
+import { useParams } from "umi"
+
+import t from "@/utils/t"
+import useFetch from "@/hooks/useFetch"
+import { humanize } from "@/utils/number"
+
+import Breadcrumbs from '@/components/common/breadcrumb'
+import Panel from "@/components/form/panel"
+import DatasetSelect from "@/components/form/datasetSelect"
+import AnalysisChart from "./components/analysisChart"
+
+import style from "./analysis.less"
+import { CompareIcon } from "@/components/common/icons"
+
+const options = [
+ { value: 'gt' },
+ { value: 'pred' }
+]
+
+function Analysis() {
+ const [form] = Form.useForm()
+ const { id: pid } = useParams()
+ const [remoteSource, fetchSource] = useFetch('dataset/analysis')
+ const [source, setSource] = useState([])
+ const [datasets, setDatasets] = useState([])
+ const [tableSource, setTableSource] = useState([])
+ const [chartsData, setChartsData] = useState([])
+ const [annotationsType, setAnnotationType] = useState(options[0].value)
+
+ useEffect(() => {
+ setTableSource(source)
+ setAnalysisData(source)
+ }, [source, annotationsType])
+
+ useEffect(() => {
+ setSource(remoteSource)
+ }, [remoteSource])
+
+ function setAnalysisData(datasets) {
+ const chartsMap = [
+ {
+ label: 'dataset.analysis.title.asset_bytes',
+ sourceField: 'assetBytes',
+ totalField: 'assetCount',
+ xUnit: 'MB',
+ renderEachX: x => x.replace("MB", ""),
+ color: ['#10BC5E', '#F2637B']
+ },
+ {
+ label: 'dataset.analysis.title.asset_hw_ratio',
+ sourceField: 'assetHWRatio',
+ totalField: 'assetCount',
+ color: ['#36CBCB', '#E8B900']
+ },
+ {
+ label: 'dataset.analysis.title.asset_area',
+ sourceField: 'assetArea',
+ totalField: 'assetCount',
+ xUnit: 'PX',
+ renderEachX: x => `${x / 10000}W`,
+ color: ['#36CBCB', '#F2637B'],
+ },
+ {
+ label: 'dataset.analysis.title.asset_quality',
+ sourceField: 'assetQuality',
+ totalField: 'assetCount',
+ color: ['#36CBCB', '#10BC5E'],
+ isXUpperLimit: true,
+ },
+ {
+ label: 'dataset.analysis.title.anno_area_ratio',
+ sourceField: 'areaRatio',
+ totalField: 'total',
+ customOptions: {
+ tooltipLable: 'dataset.analysis.bar.anno.tooltip',
+ },
+ color: ['#10BC5E', '#E8B900'],
+ annoType: true,
+ isXUpperLimit: true,
+ },
+ {
+ label: 'dataset.analysis.title.keyword_ratio',
+ sourceField: 'keywords',
+ totalField: 'total',
+ color: ['#2CBDE9', '#E8B900'],
+ annoType: true,
+ xType: 'attribute'
+ },
+ ]
+
+ const chartsConfig = datasets ? chartsMap.map(chart => {
+ const xData = chart.xType === 'attribute' ? getAttrXData(chart, datasets) : getXData(chart, datasets)
+ const yData = chart.xType === 'attribute' ? getAttrYData(chart, datasets, xData) : getYData(chart, datasets)
+ return {
+ label: chart.label,
+ customOptions: {
+ ...chart.customOptions,
+ xData,
+ color: chart.color,
+ xUnit: chart.xUnit,
+ yData
+ },
+ }
+ }) : []
+ setChartsData(chartsConfig)
+ }
+
+ const getField = (item = {}, field, annoType) => {
+ return annoType && item[annotationsType] ? item[annotationsType][field] : item[field]
+ }
+
+ function getXData({ sourceField, isXUpperLimit = false, annoType, renderEachX = x => x }, datasets) {
+ const dataset = datasets.find(item => {
+ const target = getField(item, sourceField, annoType)
+ return target && target.length > 0
+ }) || datasets[0]
+ const field = getField(dataset, sourceField, annoType)
+ const xData = field ? field.map(item => renderEachX(item.x)) : []
+ const transferXData = xData.map((x, index) => {
+ if (index === xData.length - 1) {
+ return isXUpperLimit ? x : `[${x},+)`
+ } else {
+ return `[${x},${xData[index + 1]})`
+ }
+ })
+ return transferXData
+ }
+
+ function getYData({ sourceField, annoType, totalField }, datasets) {
+ const yData = datasets && datasets.map(dataset => {
+ const total = getField(dataset, totalField, annoType)
+ const name = `${dataset.name} ${dataset.versionName}`
+ const field = getField(dataset, sourceField, annoType)
+ return {
+ name,
+ value: field.map(item => total ? (item.y / total).toFixed(4) : 0),
+ count: field.map(item => item.y)
+ }
+ })
+ return yData
+ }
+
+ function getAttrXData({ sourceField, annoType }, datasets) {
+ let xData = []
+ datasets && datasets.forEach((dataset) => {
+ const field = getField(dataset, sourceField, annoType)
+ const datasetAttrs = Object.keys(field || {})
+ xData = [...new Set([...xData, ...datasetAttrs])]
+ })
+ return xData
+ }
+
+ function getAttrYData({ sourceField, annoType, totalField }, datasets, xData) {
+ const yData = datasets && datasets.map(dataset => {
+ const total = getField(dataset, totalField, annoType)
+ const name = `${dataset.name} ${dataset.versionName}`
+ const attrObj = getField(dataset, sourceField, annoType)
+ return {
+ name,
+ value: xData.map(key => total ? (attrObj[key] ? (attrObj[key] / total).toFixed(4) : 0) : 0),
+ count: xData.map(key => attrObj[key] || 0)
+ }
+ })
+ return yData
+ }
+
+ function datasetsChange(values, options) {
+ setDatasets(options.map(option => option.dataset))
+ }
+
+ const onFinish = async (values) => {
+ const params = {
+ pid,
+ datasets: values.datasets
+ }
+ fetchSource(params)
+ }
+
+ function onFinishFailed(errorInfo) {
+ console.log("Failed:", errorInfo)
+ }
+
+ function retry() {
+ setSource(null)
+ }
+
+ function showTitle(str) {
+ return
{t(str)}
+ }
+
+ const columns = [
+ {
+ title: showTitle('dataset.analysis.column.name'),
+ dataIndex: "name",
+ ellipsis: true,
+ align: 'center',
+ className: style.colunmClass,
+ },
+ {
+ title: showTitle('dataset.analysis.column.version'),
+ dataIndex: "versionName",
+ ellipsis: true,
+ align: 'center',
+ width: 80,
+ className: style.colunmClass,
+ },
+ {
+ title: showTitle('dataset.analysis.column.size'),
+ dataIndex: "totalAssetMbytes",
+ ellipsis: true,
+ align: 'center',
+ className: style.colunmClass,
+ render: (num) => {
+ return num &&
{num}MB
+ },
+ },
+ {
+ title: showTitle('dataset.analysis.column.box_count'),
+ dataIndex: 'total',
+ ellipsis: true,
+ align: 'center',
+ className: style.colunmClass,
+ render: (_, record) => {
+ const num = getField(record, 'total', true)
+ return renderPop(humanize(num), num)
+ },
+ },
+ {
+ title: showTitle('dataset.analysis.column.average_labels'),
+ dataIndex: 'average',
+ ellipsis: true,
+ align: 'center',
+ className: style.colunmClass,
+ render: (_, record) => getField(record, 'average', true),
+ },
+ {
+ title: showTitle('dataset.analysis.column.overall'),
+ dataIndex: 'metrics',
+ ellipsis: true,
+ align: 'center',
+ className: style.colunmClass,
+ render: (text, record) => {
+ const total = record.assetCount
+ const negative = getField(record, 'negative', true)
+ return renderPop(`${humanize(total - negative)}/${humanize(total)}`, `${total - negative}/${total}`)
+ },
+ },
+ ]
+
+ function renderPop(label, content = {}) {
+ return
+ {label}
+
+ }
+
+ async function validDatasetCount(rule, value) {
+ const count = 5
+ if (value?.length > count) {
+ return Promise.reject(t('dataset.analysis.validator.dataset.count', { count }))
+ } else {
+ return Promise.resolve()
+ }
+ }
+
+ const initialValues = {}
+
+ return (
+
+
+
+
+
+
+ ({ ...opt, label: t(`annotation.${opt.value}`) }))}
+ onChange={({ target: { value } }) => setAnnotationType(value)}
+ >
+
+ record.name + record.versionName}
+ rowClassName={style.rowClass}
+ className={style.tableClass}
+ columns={columns}
+ pagination={false}
+ />
+
+ {chartsData.map(chart => (
+
+ {t(chart.label)}
+
+
+ ))}
+
+
+
+
+ retry()}>
+ {t('dataset.analysis.btn.retry')}
+
+
+
+
+
+
+
+
+
+ {t('dataset.analysis.btn.start')}
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Analysis
diff --git a/ymir/web/src/pages/dataset/analysis.less b/ymir/web/src/pages/dataset/analysis.less
new file mode 100644
index 0000000000..323d1dd155
--- /dev/null
+++ b/ymir/web/src/pages/dataset/analysis.less
@@ -0,0 +1,36 @@
+.dataContainer {
+ padding: 10px 0;
+}
+
+.filters {
+ line-height: 40px;
+}
+
+.rowData {
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ height: calc(100vh - 270px);
+ overflow-y: auto;
+ .tableClass {
+ max-width: calc(100% + 20px);
+ margin: 0 -10px;
+ }
+ .rowClass {
+ height: 40px;
+ }
+ tr.rowClass:hover > td {
+ background-color: rgba(232, 185, 0, 0.1);
+ }
+ th.colunmClass {
+ background: rgba(232, 185, 0, 0.2);
+ }
+ td.colunmClass {
+ background: rgba(232, 185, 0, 0.1);
+ }
+}
+
+.echartTitle {
+ background: rgba(0, 0, 0, 0.06);
+ text-align: center;
+ padding: 10px;
+ margin: 10px 0;
+}
\ No newline at end of file
diff --git a/ymir/web/src/pages/dataset/assets.js b/ymir/web/src/pages/dataset/assets.js
index cbe34d4b6c..94f0c96f87 100644
--- a/ymir/web/src/pages/dataset/assets.js
+++ b/ymir/web/src/pages/dataset/assets.js
@@ -1,83 +1,74 @@
-import React, { useEffect, useState } from "react"
-import { useParams, useHistory } from "umi"
-import { connect } from "dva"
-import { Select, Pagination, Image, Row, Col, Button, Space, Card, Descriptions, Tag, Modal } from "antd"
+import React, { useCallback, useEffect, useRef, useState } from "react"
+import { useParams } from "umi"
+import { Select, Pagination, Row, Col, Button, Space, Card, Tag, Modal } from "antd"
import t from "@/utils/t"
-import Breadcrumbs from "@/components/common/breadcrumb"
+import useFetch from '@/hooks/useFetch'
import { randomBetween, percent } from '@/utils/number'
+
+import Breadcrumbs from "@/components/common/breadcrumb"
import Asset from "./components/asset"
import styles from "./assets.less"
-import { ScreenIcon, TaggingIcon, TrainIcon, VectorIcon, WajueIcon, } from "@/components/common/icons"
+import GtSelector from "@/components/form/gtSelector"
+import ImageAnnotation from "@/components/dataset/imageAnnotation"
+import useWindowResize from "@/hooks/useWindowResize"
+import KeywordSelector from "./components/keywordSelector"
+import EvaluationSelector from "@/components/form/evaluationSelector"
const { Option } = Select
-function rand(n, m, exclude) {
- const result = Math.min(m, n) + Math.floor(Math.random() * Math.abs(m - n))
-
- if (result === exclude) {
- return rand(n, m, exclude)
- }
- if (result < 0) {
- return 0
- }
- return result
-}
-
-const Dataset = ({ getDataset, getAssetsOfDataset }) => {
- const { did: id } = useParams()
+const Dataset = () => {
+ const { id: pid, did: id } = useParams()
const initQuery = {
id,
- keyword: null,
+ keywords: [],
offset: 0,
limit: 20,
}
- const history = useHistory()
const [filterParams, setFilterParams] = useState(initQuery)
- const [dataset, setDataset] = useState({ id })
- const [assets, setAssets] = useState([])
- const [total, setTotal] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [assetVisible, setAssetVisible] = useState(false)
const [currentAsset, setCurrentAsset] = useState({
hash: null,
index: 0,
})
+ const listRef = useRef(null)
+ const windowWidth = useWindowResize()
+ const [dataset, getDataset] = useFetch('dataset/getDataset', {})
+ const [{ items: assets, total }, getAssets, setAssets] = useFetch('dataset/getAssetsOfDataset', { items: [], total: 0 })
- useEffect(async () => {
- const data = await getDataset(id)
- if (data) {
- setDataset(data)
- }
+ useEffect(() => {
+ getDataset({ id, verbose: true })
}, [id])
useEffect(() => {
setCurrentPage((filterParams.offset / filterParams.limit) + 1)
- filter(filterParams)
- }, [filterParams])
-
- const filterKw = (kw) => {
- const keyword = kw ? kw : undefined
- setFilterParams((params) => ({
- ...params,
- keyword,
- offset: initQuery.offset,
- }))
+ dataset.id && filter(filterParams)
+ }, [dataset, filterParams])
+
+ const filterKw = ({ type, selected }) => {
+ const s = selected.map(item => Array.isArray(item) ? item.join(':') : item)
+ if (s.length || (!s.length && filterParams.keywords.length > 0)) {
+ setFilterParams((params) => ({
+ ...params,
+ type,
+ keywords: s,
+ offset: initQuery.offset,
+ }))
+ }
}
+
const filterPage = (page, pageSize) => {
setCurrentPage(page)
const limit = pageSize
const offset = limit * (page - 1)
setFilterParams((params) => ({ ...params, offset, limit }))
}
- const filter = async (param) => {
- setAssets([])
- const { items, total } = await getAssetsOfDataset(param)
- setTotal(total)
- setAssets(items)
+ const filter = (param) => {
+ getAssets({ ...param, datasetKeywords: dataset?.keywords })
}
- const goAsset = (hash, index) => {
- setCurrentAsset({ hash, index: filterParams.offset + index})
+ const goAsset = (asset, hash, index) => {
+ setCurrentAsset({ asset, hash, index: filterParams.offset + index })
setAssetVisible(true)
}
@@ -88,8 +79,26 @@ const Dataset = ({ getDataset, getAssetsOfDataset }) => {
filterPage(page, limit)
}
- const getRate = (count) => {
- return percent(count / dataset.assetCount)
+ const filterAnnotations = useCallback(annotations => {
+ const cm = filterParams.cm || []
+ const annoType = filterParams.annoType || []
+ const gtFilter = annotation => !annoType.length || annoType.some(selected => selected === 'gt' ? annotation.gt : !annotation.gt)
+ const evaluationFilter = annotation => !cm.length || cm.includes(annotation.cm)
+ return annotations.filter(annotation => gtFilter(annotation) && evaluationFilter(annotation))
+ }, [filterParams.cm, filterParams.annoType])
+
+ const updateFilterParams = (value, field) => {
+ if (value?.length || (filterParams[field]?.length && !value?.length)) {
+ setFilterParams(query => ({
+ ...query,
+ [field]: value,
+ offset: initQuery.offset,
+ }))
+ }
+ }
+
+ const reset = () => {
+ setFilterParams(initQuery)
}
const randomPageButton = (
@@ -98,77 +107,77 @@ const Dataset = ({ getDataset, getAssetsOfDataset }) => {
)
- const renderList = (list, row = 5) => {
+ const renderList = useCallback((list, row = 5) => {
let r = 0, result = []
while (r < list.length) {
result.push(list.slice(r, r + row))
r += row
}
- return result.map((rows, index) => (
-
- {rows.map((asset, rowIndex) => (
-
- goAsset(asset.hash, index * row + rowIndex)}
- >
-
-
{
+ const h = listRef.current?.clientWidth / rows.reduce((prev, row) => {
+ return (prev + row.metadata.width / row.metadata.height)
+ }, 0)
+
+ return (
+
+ {rows.map((asset, rowIndex) => (
+
+ goAsset(asset, asset.hash, index * row + rowIndex)}
>
- {t("dataset.detail.assets.keywords.total", {
- total: asset?.keywords?.length,
- })}
-
-
- {asset.keywords.slice(0, 4).map(key => {key} )}
- {asset.keywords.length > 4 ? ... : null}
-
-
-
- ))}
-
- ))
- }
+
+
+ {t("dataset.detail.assets.keywords.total", {
+ total: asset?.keywords?.length,
+ })}
+
+
+ {asset.keywords.slice(0, 4).map(key => {key} )}
+ {asset.keywords.length > 4 ? ... : null}
+
+
+
+ ))}
+
+ )
+ }
+ )
+ }, [windowWidth, filterParams])
const renderTitle =
-
+
{dataset.name} {dataset.versionName}
{t("dataset.detail.pager.total", { total: total + '/' + dataset.assetCount })}
+ {dataset?.inferClass ? {t('dataset.detail.infer.class')}{dataset?.inferClass?.map(cls => {cls} )}
: null}
-
- {t("dataset.detail.keyword.label")}
- option.key.toLowerCase().indexOf(input.toLowerCase()) >= 0}
- >
-
- {t("common.all")}
-
-
- {dataset?.keywords?.map((key) => (
-
- {key} ({dataset.keywordsCount[key]}, {getRate(dataset.keywordsCount[key])})
-
- ))}
-
+
+
+ updateFilterParams(checked, 'annoType')} />
+ updateFilterParams(checked, 'cm')} labelAlign={'right'} />
+
+ {t('common.reset')}
+
const assetDetail = setAssetVisible(false)}
width={null} footer={null}>
-
+
return (
@@ -176,7 +185,9 @@ const Dataset = ({ getDataset, getAssetsOfDataset }) => {
{assetDetail}
- {renderList(assets)}
+
+ {renderList(assets)}
+
{
)
}
-const mapStateToProps = (state) => {
- return {
- logined: state.user.logined,
- }
-}
-
-const mapDispatchToProps = (dispatch) => {
- return {
- getDataset(id, force) {
- return dispatch({
- type: "dataset/getDataset",
- payload: { id, force },
- })
- },
- getAssetsOfDataset(payload) {
- return dispatch({
- type: "dataset/getAssetsOfDataset",
- payload,
- })
- },
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(Dataset)
+export default Dataset
diff --git a/ymir/web/src/pages/dataset/assets.less b/ymir/web/src/pages/dataset/assets.less
index e2852da07f..68393fb686 100644
--- a/ymir/web/src/pages/dataset/assets.less
+++ b/ymir/web/src/pages/dataset/assets.less
@@ -8,16 +8,23 @@
.list {
padding-top: 10px;
}
+.dataset_container {
+ background-color: rgba(0, 0, 0, 0.8);
+ padding: 2px 0;
+}
.dataset_item {
- padding: 10px;
- margin-bottom: 10px;
+ // padding: 10px;
+ // margin-bottom: 10px;
}
.dataset_img {
width: 100%;
- height: 200px;
- line-height: 194px;
- text-align: center;
- border: 1px solid rgba(0, 0, 0, 0.06);
+ height: 100%;
+ // height: 200px;
+ // line-height: 194px;
+ // text-align: center;
+ // border-color: rgba(0, 0, 0, 0.8);
+ // border-style: solid;
+ // border-width: 1px 2px;
overflow: hidden;
cursor: pointer;
position: relative;
diff --git a/ymir/web/src/pages/dataset/components/analysisChart.js b/ymir/web/src/pages/dataset/components/analysisChart.js
new file mode 100644
index 0000000000..d7f1850e9b
--- /dev/null
+++ b/ymir/web/src/pages/dataset/components/analysisChart.js
@@ -0,0 +1,121 @@
+import BarChart from "@/components/chart/bar"
+import { useEffect, useState } from "react"
+import { percent } from "@/utils/number"
+import t from "@/utils/t"
+
+const AnalysisChartBar = ({ customOptions = {}, ...resProps}) => {
+ const [option, setOption] = useState({})
+ const [series, setSeries] = useState([])
+ const {
+ xData,
+ xUnit,
+ yData,
+ seriesType = 'bar',
+ barWidth = 8,
+ grid,
+ legend,
+ color,
+ tooltipLable = 'dataset.analysis.bar.asset.tooltip',
+ yAxisFormatter = function (val) {
+ return val * 100 + '%';
+ },
+ } = customOptions
+
+ const defaultLegend = { itemHeight: 8, itemWidth: 20 }
+
+ const defaultGrid = {
+ left: '3%',
+ right: 50,
+ bottom: '3%',
+ containLabel: true
+ }
+
+ const tooltip = {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ },
+ formatter: function (params) {
+ var res = `${params[0].name}`;
+ for (var i = 0, l = params.length; i < l; i++) {
+ const indexColor = params[i].color;
+ res += ` `;
+ const name = params[i].seriesName;
+ const ratio = percent(params[i].value);
+ const amount = yData[params[i].seriesIndex].count[params[i].dataIndex];
+ res += `${name}
+ ${t(tooltipLable, { ratio, amount })} `;
+ }
+ return res;
+ }
+ }
+
+ const yAxis = [
+ {
+ type: 'value',
+ splitLine: {
+ lineStyle: {
+ type: 'dashed'
+ }
+ },
+ axisLabel: {
+ formatter: yAxisFormatter
+ }
+ }
+ ]
+
+ useEffect(async () => {
+ const transData = transferData()
+ setSeries(transData)
+ }, [customOptions])
+
+ useEffect(() => {
+ if(!series.length) {
+ setOption({})
+ return
+ }
+ const xAxis = [
+ {
+ type: 'category',
+ axisLine: {
+ show: false
+ },
+ axisTick: {
+ show: false
+ },
+ name: xUnit ? `(${xUnit})` : '',
+ data: xData,
+ axisLabel: {
+ rotate: xData.length > 10 ? 45 : 0,
+ }
+ }
+ ]
+ setOption({
+ tooltip,
+ legend: Object.assign(defaultLegend, legend),
+ grid: Object.assign(defaultGrid, grid),
+ yAxis,
+ xAxis,
+ color,
+ series,
+ })
+ }, [series])
+
+ function transferData() {
+ const series = yData.map((item) => (
+ {
+ name: item.name,
+ type: seriesType,
+ barWidth,
+ data: item.value
+ }
+ ))
+ return series
+ }
+
+ return (
+
+ )
+}
+
+export default AnalysisChartBar;
diff --git a/ymir/web/src/pages/dataset/components/asset.js b/ymir/web/src/pages/dataset/components/asset.js
index b4a91acab7..caf207a161 100644
--- a/ymir/web/src/pages/dataset/components/asset.js
+++ b/ymir/web/src/pages/dataset/components/asset.js
@@ -1,32 +1,35 @@
-import { useHistory, useParams } from "react-router"
-import React, { useEffect, useRef, useState } from "react"
+import React, { useEffect, useState } from "react"
import { Button, Card, Col, Descriptions, Row, Tag, Space } from "antd"
-import { connect } from "dva"
import { getDateFromTimestamp } from "@/utils/date"
import t from "@/utils/t"
-import Hash from "@/components/common/hash"
-import AssetAnnotation from "@/components/dataset/asset_annotation"
import { randomBetween } from "@/utils/number"
+import useFetch from '@/hooks/useFetch'
+
+import Hash from "@/components/common/hash"
+import AssetAnnotation from "@/components/dataset/assetAnnotation"
+import GtSelector from "@/components/form/gtSelector"
+
import styles from "./asset.less"
-import { ArrowRightIcon, NavDatasetIcon, EyeOffIcon, EyeOnIcon } from '@/components/common/icons'
+import { NavDatasetIcon, EyeOffIcon, EyeOnIcon } from '@/components/common/icons'
import { LeftOutlined, RightOutlined } from '@ant-design/icons'
+import EvaluationSelector from "@/components/form/evaluationSelector"
const { CheckableTag } = Tag
+const { Item } = Descriptions
-const KeywordColor = ["green", "red", "cyan", "blue", "yellow", "purple", "magenta", "orange", "gold"]
-
-function Asset({ id, datasetKeywords = [], filterKeyword, getAsset, getAssetsOfDataset, index = 0, total = 0 }) {
- const history = useHistory()
+function Asset({ id, asset: cache, datasetKeywords, filterKeyword, filters, index = 0, total = 0 }) {
const [asset, setAsset] = useState({})
const [current, setCurrent] = useState('')
const [showAnnotations, setShowAnnotations] = useState([])
const [selectedKeywords, setSelectedKeywords] = useState([])
const [currentIndex, setCurrentIndex] = useState(null)
const [assetHistory, setAssetHistory] = useState([])
- const [colors] = useState(datasetKeywords.reduce((prev, curr, i) =>
- ({ ...prev, [curr]: KeywordColor[i % KeywordColor.length] }), {}))
+ const [evaluation, setEvaluation] = useState([])
+ const [gtSelected, setGtSelected] = useState([])
+ const [colors, setColors] = useState({})
+ const [{ items: assets }, getAssets] = useFetch('dataset/getAssetsOfDataset', { items: [] })
useEffect(() => {
setAsset({})
@@ -41,36 +44,37 @@ function Asset({ id, datasetKeywords = [], filterKeyword, getAsset, getAssetsOfD
}, [currentIndex])
useEffect(() => {
- if (!current) {
- return
+ if (cache) {
+ setAsset(cache)
+ setCurrent(cache.hash)
}
- fetchAsset()
- }, [current])
+ }, [cache])
useEffect(() => {
- setShowAnnotations((asset.annotations || []).filter(anno => selectedKeywords.indexOf(anno.keyword) >= 0))
- }, [selectedKeywords])
-
- async function fetchAsset() {
- const compare = (a, b) => {
- const aa = (a.keyword || a).toUpperCase()
- const bb = (b.keyword || b).toUpperCase()
- return aa > bb ? -1 : (aa < bb ? 1 : 0)
+ if (!asset.hash) {
+ return
}
+ const { annotations } = asset
+ setSelectedKeywords(asset.keywords)
+ setCurrent(asset.hash)
+ setColors(annotations.reduce((prev, annotation) => ({ ...prev, [annotation.keyword]: annotation.color }), {}))
+ }, [asset])
- const result = await getAsset(id, current)
- const keywords = result.keywords.sort(compare)
- const annotations = result.annotations.sort(compare).map(anno => ({ ...anno, color: colors[anno.keyword] }))
- setAsset({ ...result, keywords, annotations })
- setSelectedKeywords(keywords)
- }
+ useEffect(() => {
+ assets.length && setAsset(assets[0])
+ }, [assets])
- async function fetchAssetHash() {
- const result = await getAssetsOfDataset({ id, keyword: currentIndex.keyword, offset: currentIndex.index, limit: 1 })
- if (result?.items) {
- const ass = result.items[0]
- setCurrent(ass.hash)
- }
+ useEffect(() => {
+ const keywordFilter = annotation => selectedKeywords.indexOf(annotation.keyword) >= 0
+ const gtFilter = annotation => !gtSelected?.length || gtSelected.some(selected => selected === 'gt' ? annotation.gt : !annotation.gt)
+ const evaluationFilter = annotation => !evaluation?.length || evaluation.includes(annotation.cm)
+ const filters = annotation => keywordFilter(annotation) && evaluationFilter(annotation) && gtFilter(annotation)
+ const visibleAnnotations = (asset.annotations || []).filter(filters)
+ setShowAnnotations(visibleAnnotations)
+ }, [selectedKeywords, evaluation, asset, gtSelected])
+
+ function fetchAssetHash() {
+ getAssets({ id, ...filters, keyword: currentIndex.keyword, offset: currentIndex.index, limit: 1, datasetKeywords })
}
function next() {
@@ -82,7 +86,7 @@ function Asset({ id, datasetKeywords = [], filterKeyword, getAsset, getAssetsOfD
}
function random() {
- setCurrentIndex(cu => ({ ...cu, index: randomBetween(0, total - 1, cu.index)}))
+ setCurrentIndex(cu => ({ ...cu, index: randomBetween(0, total - 1, cu.index) }))
}
function back() {
@@ -109,8 +113,10 @@ function Asset({ id, datasetKeywords = [], filterKeyword, getAsset, getAssetsOfD
-
-
+
+
+
+
{asset.annotations ? (
{t("dataset.asset.info")}>}
bordered={false}
+ className='noShadow'
style={{ marginRight: 20 }}
headStyle={{ paddingLeft: 0 }}
bodyStyle={{ padding: "20px 0" }}
@@ -134,27 +141,27 @@ function Asset({ id, datasetKeywords = [], filterKeyword, getAsset, getAssetsOfD
contentStyle={{ flexWrap: 'wrap', padding: '10px' }}
labelStyle={{ justifyContent: 'flex-end', padding: '10px' }}
>
-
+ -
-
-
+
+ -
{asset.metadata?.width}
-
-
+
+ -
{asset.metadata?.height}
-
+
{asset.size ? (
-
+ -
{asset.size}
-
+
) : null}
-
- {asset.metadata?.channel}
-
-
- {getDateFromTimestamp(asset.metadata?.timestamp)}
-
-
+ -
+ {asset.metadata?.image_channels}
+
+ -
+ {getDateFromTimestamp(asset.metadata?.timestamp?.start)}
+
+ -
{asset.keywords?.map((keyword, i) => (
@@ -163,6 +170,7 @@ function Asset({ id, datasetKeywords = [], filterKeyword, getAsset, getAssetsOfD
onChange={(checked) => changeKeywords(keyword, checked)}
className={'ant-tag-' + colors[keyword]}
key={i}
+ color={colors[keyword]}
>
{keyword}
@@ -174,8 +182,18 @@ function Asset({ id, datasetKeywords = [], filterKeyword, getAsset, getAssetsOfD
}
-
+
+ -
+ {Object.keys(asset.cks).map(ck =>
+ {ck}: {asset.cks[ck]}
+ )}
+
+
+
+
+
+
- = total - 1} className={styles.next} onClick={next} />
+
+ = total - 1} className={styles.next} onClick={next} />
+
@@ -203,21 +223,4 @@ function Asset({ id, datasetKeywords = [], filterKeyword, getAsset, getAssetsOfD
)
}
-const actions = (dispatch) => {
- return {
- getAsset(id, hash) {
- return dispatch({
- type: "dataset/getAsset",
- payload: { id, hash },
- })
- },
- getAssetsOfDataset(payload) {
- return dispatch({
- type: "dataset/getAssetsOfDataset",
- payload,
- })
- },
- }
-}
-
-export default connect(null, actions)(Asset)
+export default Asset
diff --git a/ymir/web/src/pages/dataset/components/asset.less b/ymir/web/src/pages/dataset/components/asset.less
index 67cbdb16f0..49b5a28249 100644
--- a/ymir/web/src/pages/dataset/components/asset.less
+++ b/ymir/web/src/pages/dataset/components/asset.less
@@ -11,35 +11,28 @@
}
.asset_info {
position: relative;
- .random, .back {
- position: absolute;
- bottom: 0;
- }
+ overflow-y: auto;
+ // .random {
+ // position: absolute;
+ // bottom: 0;
+ // }
.back {
- left: 80px;
+ left: 10px;
}
}
}
+.filter {
+ margin-top: 20px;
+}
.title {
border-bottom: 1px solid #ccc;
padding-bottom: 10px;
font-weight: bold;
}
.asset_img {
- overflow-y: auto;
height: 100%;
margin-right: 20px;
padding: 20px;
border: 1px solid rgba(0, 0, 0, 0.06);
text-align: center;
- &::-webkit-scrollbar {
- width: 1px;
- background-color: #ddd;
- }
- &::-webkit-scrollbar-thumb {
- width: 10px;
- border: 1px solid #0000ff10;
- background-color: #00000005;
- border-radius: 10px;
- }
}
diff --git a/ymir/web/src/pages/dataset/components/formatDetailModal.js b/ymir/web/src/pages/dataset/components/formatDetailModal.js
new file mode 100644
index 0000000000..ae5a578292
--- /dev/null
+++ b/ymir/web/src/pages/dataset/components/formatDetailModal.js
@@ -0,0 +1,79 @@
+import { Card, Modal } from "antd"
+import { useState } from "react"
+import XMLViewer from 'react-xml-viewer'
+
+const annotationsFormat = `
+VOC_ROOT
+aaaa.jpg # filename(required)
+ # image szie(width, height and channel count)
+ 500
+ 332
+ 3
+
+1.234 # image quality (optional)
+1 # for segmentation(optional)
+ # object
+ horse # object class
+ Unspecified # shooting angle(optional)
+ 0 # truncated
+ 0 # recognition degree, 0(easy), 1(difficult) (optional)
+ 1.234 # box quality
+ # bounding-box
+ 100
+ 96
+ 355
+ 324
+ 2.889813
+
+
+ # multiple objects
+ person
+ Unspecified
+ 0
+ 0
+ 1.234
+ 0.95 # box confidence (for prediction, not VOC standard attribute) (optional)
+
+ 100
+ 96
+ 355
+ 324
+ 2.889813
+
+
+ `
+
+const tabs = [
+ { tab: '*.xml', key: 'xml', },
+ { tab: 'meta.yaml', key: 'yaml', },
+]
+
+const contents = {
+ 'xml': ,
+ 'yaml':
+
{
+ `
+eval_class_names:
+ - person
+ - cat
+ `
+ }
+
+
,
+}
+
+export const FormatDetailModal = props => {
+ const [active, setActive] = useState('xml')
+ return (
+
+
+ {contents[active]}
+
+
+ )
+}
diff --git a/ymir/web/src/pages/dataset/components/keywordSelector.js b/ymir/web/src/pages/dataset/components/keywordSelector.js
new file mode 100644
index 0000000000..71b6080558
--- /dev/null
+++ b/ymir/web/src/pages/dataset/components/keywordSelector.js
@@ -0,0 +1,105 @@
+import { Cascader, Col, Row, Select } from "antd"
+import { useParams } from "umi"
+import t from "@/utils/t"
+import { useEffect, useState } from "react"
+
+import useFetch from '@/hooks/useFetch'
+
+const initKeywords = [
+ { value: 'keywords', list: [], },
+ { value: 'cks', list: [], },
+ { value: 'tags', list: [], }
+]
+
+const KeywordSelector = ({ value, onChange, dataset = {} }) => {
+ const { id: pid } = useParams()
+ const [keywords, setKeywords] = useState(initKeywords)
+ const [currentType, setCurrentType] = useState(initKeywords[0].value)
+ const [selected, setSelected] = useState([])
+ const { cks, tags } = dataset
+
+ useEffect(() => {
+ !value?.length && setSelected([])
+ }, [value])
+
+ useEffect(() => {
+ if (!dataset.id) {
+ return
+ }
+ generateKeywords(initKeywords[0].value, dataset.keywords.map(keyword => ({ keyword })))
+ }, [dataset])
+
+ useEffect(() => {
+ generateKeywords(initKeywords[1].value, cks?.keywords)
+ }, [cks])
+
+ useEffect(() => {
+ generateKeywords(initKeywords[2].value, tags?.keywords)
+ }, [tags])
+
+ useEffect(() => {
+ onChange({ type: currentType, selected })
+ }, [selected])
+
+ useEffect(() => {
+ setSelected([])
+ }, [currentType])
+
+ function generateKeywords(type, kws = []) {
+ return setKeywords(keywords => keywords.map((item) => ({
+ ...item,
+ list: item.value === type ? kws : item.list
+ })))
+ }
+
+ const renderKeywords = (type) => {
+ const { list = [] } = keywords.find(({ value }) => value === type) || {}
+ return type !== 'keywords' ? renderCk(list) : renderKw(list)
+ }
+
+ const renderKw = (list = []) =>
+
+ const renderCk = (list = []) => value.join('/')}
+ placeholder={t('dataset.assets.keyword.selector.types.placeholder')}
+ />
+
+
+ return (
+
+
+ ({
+ value,
+ label: t(`dataset.assets.keyword.selector.types.${value}`)
+ }))}
+ />
+
+
+ {renderKeywords(currentType)}
+
+
+ )
+}
+
+export default KeywordSelector
diff --git a/ymir/web/src/pages/dataset/detail.js b/ymir/web/src/pages/dataset/detail.js
index ec86166713..1a26bb5894 100644
--- a/ymir/web/src/pages/dataset/detail.js
+++ b/ymir/web/src/pages/dataset/detail.js
@@ -1,27 +1,35 @@
import React, { useEffect, useRef, useState } from "react"
-import { connect } from "dva"
-import { useHistory, useParams, Link } from "umi"
-import { Button, Card, Space } from "antd"
+import { useHistory, useParams, Link, useSelector } from "umi"
+import { Button, Card, message, Space } from "antd"
import t from "@/utils/t"
import { TASKTYPES, getTaskTypeLabel } from "@/constants/task"
+import useFetch from '@/hooks/useFetch'
+import useRestore from "@/hooks/useRestore"
+import { canHide } from '@/constants/dataset'
+
import Breadcrumbs from "@/components/common/breadcrumb"
import TaskDetail from "@/components/task/detail"
import Detail from "@/components/dataset/detail"
-import s from "./detail.less"
import TaskProgress from "@/components/task/progress"
import Error from "@/components/task/error"
import Hide from "@/components/common/hide"
-import useRestore from "@/hooks/useRestore"
+import useCardTitle from '@/hooks/useCardTitle'
-const taskTypes = ["fusion", "train", "mining", "label", 'inference', 'copy']
+import s from "./detail.less"
+import useRerunAction from "../../hooks/useRerunAction"
+
+const taskTypes = ["merge", "filter", "train", "mining", "label", 'inference', 'copy']
-function DatasetDetail({ datasetCache, getDataset }) {
+function DatasetDetail() {
const history = useHistory()
const { id: pid, did: id } = useParams()
- const [dataset, setDataset] = useState({})
+ const [dataset, getDataset, setDataset] = useFetch('dataset/getDataset', {})
+ const datasetCache = useSelector(({ dataset }) => dataset.dataset)
const hideRef = useRef(null)
const restoreAction = useRestore(pid)
+ const generateRerunBtn = useRerunAction('btn')
+ const cardTitle = useCardTitle('dataset.detail.title')
useEffect(() => {
fetchDataset(true)
@@ -35,8 +43,8 @@ function DatasetDetail({ datasetCache, getDataset }) {
}
}, [datasetCache])
- async function fetchDataset(force) {
- await getDataset(id, force)
+ function fetchDataset(force) {
+ getDataset({ id, verbose: true, force })
}
const hide = (version) => {
@@ -61,15 +69,21 @@ function DatasetDetail({ datasetCache, getDataset }) {
" + t(getTaskTypeLabel(dataset.taskType))}
+ title={cardTitle}
>
- fetchDataset(true)} />
- {dataset?.task?.error_code ? : null}
+ fetchDataset(true)}
+ />
+
{dataset.taskType === TASKTYPES.LABEL ? (
@@ -80,27 +94,24 @@ function DatasetDetail({ datasetCache, getDataset }) {
) : null}
{!dataset.hidden ? <>
- {taskTypes.map((type) => (
+ {taskTypes.map((type, index) => index === 0 || dataset.assetCount > 0 ? (
history.push(`/home/task/${type}/${pid}?did=${id}`)}
+ onClick={() => history.push(`/home/project/${pid}/${type}?did=${id}`)}
>
- {t(`task.type.${type}`)}
+ {t(`common.action.${type}`)}
- ))}
- hide(dataset)}>
+ ) : null)}
+ {canHide(dataset) ? hide(dataset)}>
{t(`common.action.hide`)}
-
- history.push(`/home/project/${pid}/dataset/${dataset.groupId}/compare/${id}`)}>
- {t(`common.action.compare`)}
-
+ : null}
> :
{t("common.action.restore")}
}
-
+ {generateRerunBtn(dataset)}
@@ -109,21 +120,4 @@ function DatasetDetail({ datasetCache, getDataset }) {
)
}
-const props = (state) => {
- return {
- datasetCache: state.dataset.dataset,
- }
-}
-
-const actions = (dispatch) => {
- return {
- getDataset: (id, force) => {
- return dispatch({
- type: "dataset/getDataset",
- payload: { id, force },
- })
- },
- }
-}
-
-export default connect(props, actions)(DatasetDetail)
+export default DatasetDetail
diff --git a/ymir/web/src/pages/image/add.js b/ymir/web/src/pages/image/add.js
index bf900c1e07..6bf2709a9f 100644
--- a/ymir/web/src/pages/image/add.js
+++ b/ymir/web/src/pages/image/add.js
@@ -5,8 +5,8 @@ import { useParams, useHistory, useLocation } from "umi"
import s from './add.less'
import t from '@/utils/t'
+import { formLayout } from "@/config/antd"
import Breadcrumbs from '@/components/common/breadcrumb'
-import Tip from '@/components/form/tip'
const { useForm } = Form
@@ -60,7 +60,7 @@ const Add = ({ getImage, createImage, updateImage }) => {
}
const checkImageUrl = (_, value) => {
- const reg = /^([a-zA-Z0-9]{4,30}\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*(:[a-zA-Z0-9._-]+)?$/
+ const reg = /^[^\s]+$/
if (!value || reg.test(value.trim())) {
return Promise.resolve()
}
@@ -110,57 +110,52 @@ const Add = ({ getImage, createImage, updateImage }) => {
-
+
+
+
+ setUserInput(true)} />
+
+
+
+
+
+
+
+
+ {isEdit ? t('image.update.submit') : t('image.add.submit')}
+
+
+
+ history.goBack()}>
+ {t('common.back')}
+
+
+
+
diff --git a/ymir/web/src/pages/image/components/del.js b/ymir/web/src/pages/image/components/del.js
index c198c88c4c..1322e5651e 100644
--- a/ymir/web/src/pages/image/components/del.js
+++ b/ymir/web/src/pages/image/components/del.js
@@ -1,9 +1,19 @@
+import { forwardRef, useEffect, useImperativeHandle } from "react"
+
import t from "@/utils/t"
+import useFetch from '@/hooks/useFetch'
+
import confirm from '@/components/common/dangerConfirm'
-import { connect } from "dva"
-import { forwardRef, useImperativeHandle } from "react"
-const Del = forwardRef(({ delImage, ok = () => {} }, ref) => {
+const Del = forwardRef(({ ok = () => {} }, ref) => {
+ const [delResult, delImage] = useFetch('image/delImage')
+
+ useEffect(() => {
+ if (delResult) {
+ ok(delResult.id)
+ }
+ }, [delResult])
+
useImperativeHandle(ref, () => {
return {
del,
@@ -13,12 +23,7 @@ const Del = forwardRef(({ delImage, ok = () => {} }, ref) => {
function del(id, name) {
confirm({
content: t("image.del.confirm.content", { name }),
- onOk: async () => {
- const result = await delImage(id)
- if (result) {
- ok(id)
- }
- },
+ onOk: () => delImage(id),
okText: t('common.del'),
})
}
@@ -26,15 +31,4 @@ const Del = forwardRef(({ delImage, ok = () => {} }, ref) => {
return null
})
-const actions = (dispatch) => {
- return {
- delImage(id) {
- return dispatch({
- type: 'image/delImage',
- payload: id,
- })
- }
- }
-}
-
-export default connect(null, actions, null, { forwardRef: true })(Del)
\ No newline at end of file
+export default Del
\ No newline at end of file
diff --git a/ymir/web/src/pages/image/components/list.js b/ymir/web/src/pages/image/components/list.js
index 76fa8573f1..2a2b8017be 100644
--- a/ymir/web/src/pages/image/components/list.js
+++ b/ymir/web/src/pages/image/components/list.js
@@ -5,6 +5,7 @@ import { useHistory } from "umi"
import { List, Skeleton, Space, Button, Pagination, Col, Row, } from "antd"
import t from "@/utils/t"
+import { HIDDENMODULES } from '@/constants/common'
import { ROLES } from '@/constants/user'
import { TYPES, STATES, getImageTypeLabel, imageIsPending } from '@/constants/image'
import ShareModal from "./share"
@@ -19,6 +20,7 @@ import { LoadingOutlined } from '@ant-design/icons'
const initQuery = {
name: undefined,
type: undefined,
+ current: 1,
offset: 0,
limit: 20,
}
@@ -45,7 +47,7 @@ const ImageList = ({ role, filter, getImages }) => {
const pageChange = (current, pageSize) => {
const limit = pageSize
const offset = (current - 1) * pageSize
- setQuery((old) => ({ ...old, limit, offset }))
+ setQuery((old) => ({ ...old, current, limit, offset }))
}
async function getData() {
@@ -62,7 +64,7 @@ const ImageList = ({ role, filter, getImages }) => {
}
const moreList = (record) => {
- const { id, name, state, functions, url, related, is_shared } = record
+ const { id, name, state, functions, url, related, isShared } = record
const menus = [
{
@@ -76,7 +78,7 @@ const ImageList = ({ role, filter, getImages }) => {
key: "share",
label: t("image.action.share"),
onclick: () => share(id, name),
- hidden: () => !isDone(state) || is_shared,
+ hidden: () => !isDone(state) || isShared,
icon: ,
},
{
@@ -153,7 +155,11 @@ const ImageList = ({ role, filter, getImages }) => {
[STATES.DONE]: ,
[STATES.ERROR]: ,
}
- return states[state]
+ return {states[state]}
+ }
+
+ const liveCodeState = (live) => {
+ return {t(live ? 'image.livecode.label.remote' : 'image.livecode.label.local')}
}
const addBtn = (
@@ -162,14 +168,18 @@ const ImageList = ({ role, filter, getImages }) => {
const renderItem = (item) => {
const title =
- {item.name}{imageState(item.state)}
+
+ {item.name}
+ {imageState(item.state)}
+ {isDone(item.state) && !HIDDENMODULES.LIVECODE ? liveCodeState(item.liveCode) : null}
+
{more(item)}
const type = isTrain(item.functions) ? 'train' : 'mining'
const desc =
-
- {t('image.list.item.type')} {getImageTypeLabel(item.functions).map(label => t(label)).join(', ')}
- {t('image.list.item.url')} {item.url}
+
+ {t('image.list.item.type')} {getImageTypeLabel(item.functions).map(label => t(label)).join(', ')}
+ {t('image.list.item.url')} {item.url}
{t('image.list.item.desc')} {item.description}
{isTrain(item.functions) && item.related?.length ? {t('image.list.item.related')}
: null}
@@ -193,7 +203,8 @@ const ImageList = ({ role, filter, getImages }) => {
renderItem={renderItem}
/>
t('image.list.total', { total })}
showQuickJumper showSizeChanger />
diff --git a/ymir/web/src/pages/image/components/list.less b/ymir/web/src/pages/image/components/list.less
index a44f2c6452..950b2a075b 100644
--- a/ymir/web/src/pages/image/components/list.less
+++ b/ymir/web/src/pages/image/components/list.less
@@ -19,3 +19,20 @@
display: flex;
justify-content: flex-end;
}
+.remote, .local {
+ display: inline-block;
+ padding: 0 8px;
+ font-size: 14px;
+ color: #fff;
+ border-radius: 46px;
+ font-weight: normal;
+}
+.remote {
+ background-color: @btn-primary-bg;
+}
+.local {
+ background-color: @primary-color;
+}
+.info {
+ margin: 10px 0;
+}
diff --git a/ymir/web/src/pages/image/components/relate.js b/ymir/web/src/pages/image/components/relate.js
index 73c83fa8e5..38b5de5de1 100644
--- a/ymir/web/src/pages/image/components/relate.js
+++ b/ymir/web/src/pages/image/components/relate.js
@@ -1,26 +1,37 @@
-import { Modal, Form, Input, Select, message } from "antd"
+import { Modal, Form, Select, message } from "antd"
import { forwardRef, useEffect, useState, useImperativeHandle } from "react"
-import { connect } from 'dva'
import t from '@/utils/t'
-import { TYPES, STATES } from '@/constants/image'
+import { TYPES } from '@/constants/image'
+import useFetch from '@/hooks/useFetch'
const { useForm } = Form
-const RelateModal = forwardRef(({ getMiningImage, relate, ok = () => { } }, ref) => {
+const RelateModal = forwardRef(({ ok = () => { } }, ref) => {
const [visible, setVisible] = useState(false)
const [links, setLinks] = useState([])
- const [images, setImages] = useState([])
const [id, setId] = useState(null)
const [imageName, setImageName] = useState('')
const [linkForm] = useForm()
+ const [relateResult, relate] = useFetch('image/relateImage')
+ const [{ items: images }, getMiningImages] = useFetch('image/getImages', { items: [] })
- useEffect(() => {
- linkForm.setFieldsValue({ relations: links.map(image => image.id) })
- }, [links, visible])
+ useEffect(() => linkForm.setFieldsValue({
+ relations: links.map(image => image.id)
+ }), [links, visible])
+
+ useEffect(() => visible && getMiningImages({
+ type: TYPES.MINING,
+ offset: 0,
+ limit: 10000,
+ }), [visible])
useEffect(() => {
- visible && fetchMiningImages()
- }, [visible])
+ if (relateResult) {
+ message.success(t('image.link.success'))
+ setVisible(false)
+ ok()
+ }
+ }, [relateResult])
useImperativeHandle(ref, () => ({
show: ({ id, name, related }) => {
@@ -34,25 +45,20 @@ const RelateModal = forwardRef(({ getMiningImage, relate, ok = () => { } }, ref)
const linkModalCancel = () => setVisible(false)
const submitLink = () => {
- linkForm.validateFields().then(async () => {
+ linkForm.validateFields().then(() => {
const { relations } = linkForm.getFieldValue()
- const result = await relate(id, relations)
- if (result) {
- message.success(t('image.link.success'))
- setVisible(false)
- ok()
- }
+ relate({ id, relations })
})
}
- async function fetchMiningImages() {
- const result = await getMiningImage()
- if (result) {
- setImages(result.items)
- }
- }
-
- return
+ return
})
-const props = (state) => {
- return {
- username: state.user.username,
- }
-}
-const actions = (dispatch) => {
- return {
- getImageRelated(id) {
- return dispatch({
- type: 'image/getImageRelated',
- payload: id,
- })
- },
- relate(id, relations) {
- return dispatch({
- type: 'image/relateImage',
- payload: { id, relations },
- })
- },
- getMiningImage() {
- return dispatch({
- type: 'image/getImages',
- payload: { type: TYPES.MINING, offset: 0, limit: 10000, },
- })
- }
- }
-}
-export default connect(props, actions, null, { forwardRef: true })(RelateModal)
+export default RelateModal
diff --git a/ymir/web/src/pages/image/components/share.js b/ymir/web/src/pages/image/components/share.js
index 1015fcd6bb..e146bc6529 100644
--- a/ymir/web/src/pages/image/components/share.js
+++ b/ymir/web/src/pages/image/components/share.js
@@ -1,20 +1,31 @@
import { Modal, Form, Input, message } from "antd"
import { useEffect, useState, forwardRef, useImperativeHandle } from "react"
-import { connect } from 'dva'
+import { useSelector } from 'umi'
import t from '@/utils/t'
import { phoneValidate } from "@/components/form/validators"
+import useFetch from '@/hooks/useFetch'
const { useForm } = Form
-const ShareModal = forwardRef(({ username, email, phone, ok = () => {}, shareImage }, ref) => {
+const ShareModal = forwardRef(({ ok = () => { } }, ref) => {
const [shareForm] = useForm()
const [visible, setVisible] = useState(false)
const [id, setId] = useState(null)
const [imageName, setImageName] = useState('')
+ const { username, email, phone } = useSelector(({ user }) => user)
+ const [shareResult, shareImage] = useFetch('image/shareImage')
useEffect(() => {
shareForm.setFieldsValue({ email, phone })
- }, [email, phone ])
+ }, [email, phone])
+
+ useEffect(() => {
+ if (shareResult) {
+ message.success(t('image.share.success'))
+ setVisible(false)
+ ok()
+ }
+ }, [shareResult])
useImperativeHandle(ref, () => ({
show: (id, name) => {
@@ -35,68 +46,51 @@ const ShareModal = forwardRef(({ username, email, phone, ok = () => {}, shareIma
...other,
org: (org || '').trim(),
}
- const result = await shareImage(params)
- if (result) {
- message.success(t('image.share.success'))
- setVisible(false)
- ok()
- }
+ shareImage(params)
})
}
- return
- {username} */}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
})
-const props = (state) => {
- return {
- username: state.user.username,
- phone: state.user.phone,
- email: state.user.email,
- }
-}
-const actions = (dispatch) => {
- return {
- shareImage(payload) {
- return dispatch({
- type: 'image/shareImage',
- payload,
- })
- }
- }
-}
-export default connect(props, actions, null, { forwardRef: true })(ShareModal)
+export default ShareModal
diff --git a/ymir/web/src/pages/image/components/shareImageList.js b/ymir/web/src/pages/image/components/shareImageList.js
index 87565b451c..6dbc05d0b8 100644
--- a/ymir/web/src/pages/image/components/shareImageList.js
+++ b/ymir/web/src/pages/image/components/shareImageList.js
@@ -41,8 +41,8 @@ const ImageList = ({ role, getShareImages }) => {
return isAdmin() ? menus : []
}
- function copy (record) {
- history.push({pathname: '/home/image/add', state: { record }})
+ function copy(record) {
+ history.push({ pathname: '/home/image/add', state: { record } })
}
function isAdmin() {
@@ -76,11 +76,11 @@ const ImageList = ({ role, getShareImages }) => {
{t('image.list.item.type')} {item.functions}
{t('image.list.item.desc')} {item.description}
+
+
+ {item.organization}
+ {item.contributor}
-
- {item.organization}
- {item.contributor}
-
diff --git a/ymir/web/src/pages/image/detail.js b/ymir/web/src/pages/image/detail.js
index 23bc6a0526..86425b050c 100644
--- a/ymir/web/src/pages/image/detail.js
+++ b/ymir/web/src/pages/image/detail.js
@@ -1,39 +1,38 @@
-import React, { useEffect, useRef, useState } from "react"
-import { Descriptions, List, Space, Tag, Card, Button, Row, Col } from "antd"
-import { connect } from 'dva'
-import { useParams, Link, useHistory } from "umi"
+import React, { useEffect, useRef } from "react"
+import { Descriptions, Space, Card, Button, Row, Col } from "antd"
+import { useParams, Link, useHistory, useSelector } from "umi"
import t from "@/utils/t"
-import Breadcrumbs from "@/components/common/breadcrumb"
import { TYPES, STATES, getImageTypeLabel } from '@/constants/image'
import { ROLES } from '@/constants/user'
+import useFetch from '@/hooks/useFetch'
+
+import Breadcrumbs from "@/components/common/breadcrumb"
import LinkModal from "./components/relate"
import ShareModal from "./components/share"
import Del from './components/del'
+import ImagesLink from "./components/imagesLink"
+import StateTag from '@/components/task/stateTag'
+
import styles from "./detail.less"
import { EditIcon, VectorIcon, TrainIcon, } from '@/components/common/icons'
-import ImagesLink from "./components/imagesLink"
-import StateTag from '../../components/task/stateTag'
const { Item } = Descriptions
-function ImageDetail({ role, getImage }) {
+function ImageDetail() {
const { id } = useParams()
const history = useHistory()
- const [image, setImage] = useState({ id })
+ // const [image, setImage] = useState({ id })
const shareModalRef = useRef(null)
const linkModalRef = useRef(null)
const delRef = useRef(null)
+ const [image, getImage] = useFetch('image/getImage', { id })
+ const role = useSelector(({ user }) => user.role)
- useEffect(async () => {
- fetchImage()
- }, [id])
+ useEffect(fetchImage, [id])
- async function fetchImage() {
- const result = await getImage(id)
- if (result) {
- setImage(result)
- }
+ function fetchImage() {
+ getImage(id)
}
function relateImage() {
@@ -70,10 +69,10 @@ function ImageDetail({ role, getImage }) {
function renderConfigs(configs = []) {
return configs.map(({config, type }) => {
- return <>
+ return
{t(getImageTypeLabel([type])[0])}
{renderConfig(config)}
- >
+
})
}
@@ -111,7 +110,7 @@ function ImageDetail({ role, getImage }) {
- {image.name}
- {getImageTypeLabel(image.functions).map(label => t(label)).join(',')}
- {image.url}
- - {image.is_shared ? t('common.yes') : t('common.no')}
+ - {image.isShared ? t('common.yes') : t('common.no')}
-
{isAdmin() && isDone() ? relateImage()}>{t('image.detail.relate')} : null}
@@ -136,22 +135,4 @@ function ImageDetail({ role, getImage }) {
)
}
-
-const props = (state) => {
- return {
- role: state.user.role,
- }
-}
-
-const actions = (dispatch) => {
- return {
- getImage(id) {
- return dispatch({
- type: 'image/getImage',
- payload: id,
- })
- },
- }
-}
-
-export default connect(props, actions)(ImageDetail)
+export default ImageDetail
diff --git a/ymir/web/src/pages/iteration/initModel.js b/ymir/web/src/pages/iteration/initModel.js
index b28bd6d637..eb68486932 100644
--- a/ymir/web/src/pages/iteration/initModel.js
+++ b/ymir/web/src/pages/iteration/initModel.js
@@ -1,15 +1,13 @@
-import { Button, Card, Form, message, Select, Space, ConfigProvider } from 'antd'
+import { Button, Card, Form, message, Space } from 'antd'
import { connect } from 'dva'
import { useEffect, useState } from 'react'
import { useParams, useHistory } from 'umi'
import { formLayout } from "@/config/antd"
import t from '@/utils/t'
-import EmptyStateModel from '@/components/empty/model'
import ModelSelect from "@/components/form/modelSelect"
import s from './add.less'
import Breadcrumbs from '@/components/common/breadcrumb'
-import Tip from "@/components/form/tip"
const { useForm } = Form
@@ -44,16 +42,16 @@ const InitModel = ({ projects = {}, ...props }) => {
const result = await props.updateProject(params)
if (result) {
message.success(t('project.initmodel.success.msg'))
- history.push(`/home/project/detail/${id}`)
+ history.goBack()
}
}
function initForm(project = {}) {
- const { model } = project
+ const { model, modelStage } = project
if (model) {
form.setFieldsValue({
- model,
+ modelStage,
})
}
}
@@ -78,18 +76,16 @@ const InitModel = ({ projects = {}, ...props }) => {
labelAlign={'left'}
colon={false}
>
- }>
-
-
-
-
+
+
+
diff --git a/ymir/web/src/pages/keyword/index.js b/ymir/web/src/pages/keyword/index.js
index 9dff33d8c5..31e49a6dda 100644
--- a/ymir/web/src/pages/keyword/index.js
+++ b/ymir/web/src/pages/keyword/index.js
@@ -24,6 +24,7 @@ const { useForm } = Form
const initQuery = {
name: "",
+ current: 1,
offset: 0,
limit: 20,
}
@@ -94,7 +95,7 @@ function Keyword({ getKeywords }) {
const pageChange = ({ current, pageSize }) => {
const limit = pageSize
const offset = (current - 1) * pageSize
- setQuery((old) => ({ ...old, limit, offset }))
+ setQuery((old) => ({ ...old, current, limit, offset }))
}
function showTitle(str) {
@@ -233,7 +234,8 @@ function Keyword({ getKeywords }) {
// total: 500,
defaultPageSize: query.limit,
showTotal: (total) => t("keyword.pager.total.label", { total }),
- defaultCurrent: 1,
+ defaultCurrent: query.current,
+ current: query.current,
}}
columns={columns}
> |
diff --git a/ymir/web/src/pages/keyword/multiAdd.js b/ymir/web/src/pages/keyword/multiAdd.js
index 1d66c3f4c4..8a5469d9b5 100644
--- a/ymir/web/src/pages/keyword/multiAdd.js
+++ b/ymir/web/src/pages/keyword/multiAdd.js
@@ -28,8 +28,7 @@ const MultiAdd = forwardRef(({ addKeywords, ok = () => { } }, ref) => {
form.resetFields()
ok()
} else {
- message.error(t('keyword.name.repeat'))
- setRepeats(result.failed || [])
+ message.error(`${t('keyword.name.repeat')}: ${(result.failed || []).join(',')}`)
}
} else {
message.error(t('keyword.add.failure'))
diff --git a/ymir/web/src/pages/model/add.js b/ymir/web/src/pages/model/add.js
index b1d1ec8833..f69e61e429 100644
--- a/ymir/web/src/pages/model/add.js
+++ b/ymir/web/src/pages/model/add.js
@@ -2,14 +2,18 @@ import { useEffect, useState } from 'react'
import { Button, Card, Form, Input, message, Modal, Select, Space, Upload } from 'antd'
import { useParams, connect, useHistory, useLocation } from 'umi'
+import { formLayout } from "@/config/antd"
import t from '@/utils/t'
import { generateName } from '@/utils/string'
+import useFetch from '@/hooks/useFetch'
+
+import { urlValidator } from '@/components/form/validators'
import Breadcrumbs from '@/components/common/breadcrumb'
-import Tip from "@/components/form/tip"
import ProjectSelect from "@/components/form/projectModelSelect"
+import Desc from "@/components/form/desc"
import Uploader from '@/components/form/uploader'
+
import s from './add.less'
-import { urlValidator } from '@/components/form/validators'
const { Option } = Select
const { useForm } = Form
@@ -21,7 +25,7 @@ const TYPES = Object.freeze({
NET: 3,
})
-const Add = ({ importModel }) => {
+const Add = () => {
const types = [
{ id: TYPES.COPY, label: t('model.add.types.copy') },
{ id: TYPES.NET, label: t('model.add.types.net') },
@@ -30,7 +34,8 @@ const Add = ({ importModel }) => {
const history = useHistory()
const { query } = useLocation()
- const { mid } = query
+ const { mid, from, stepKey } = query
+ const iterationContext = from === 'iteration'
const { id: pid } = useParams()
const [form] = useForm()
const [path, setPath] = useState('')
@@ -39,8 +44,27 @@ const Add = ({ importModel }) => {
name: generateName('import_model'),
modelId: Number(mid) ? [Number(pid), Number(mid)] : undefined,
}
+ const [importResult, importModel] = useFetch('model/importModel')
+ const [updateResult, updateProject] = useFetch('project/updateProject')
+
+ useEffect(() => {
+ if (updateResult) {
+ history.replace(`/home/project/${pid}/iterations`)
+ }
+ }, [updateResult])
+
+ useEffect(() => {
+ if (importResult) {
+ message.success(t('model.add.success'))
+ if (iterationContext && stepKey) {
+ return updateProject({ id: pid, [stepKey]: [importResult.id] })
+ }
+ const group = importResult.model_group_id || ''
+ history.push(`/home/project/${pid}/model#${group}`)
+ }
+ }, [importResult])
- async function submit(values) {
+ function submit(values) {
const params = {
...values,
projectId: pid,
@@ -56,11 +80,7 @@ const Add = ({ importModel }) => {
if (values.modelId) {
params.modelId = values.modelId[values.modelId.length - 1]
}
- const result = await importModel(params)
- if (result) {
- message.success(t('model.add.success'))
- history.push(`/home/project/detail/${pid}#model`)
- }
+ importModel(params)
}
const typeChange = (type) => {
@@ -75,90 +95,74 @@ const Add = ({ importModel }) => {
-
+
+
+
+ typeChange(value)} defaultValue={TYPES.LOCAL}>
+ {types.map(type => (
+ {type.label}
+ ))}
+
+
{isType(TYPES.COPY) ?
- <>
-
-
-
-
-
- >
+
+
+
: null}
{isType(TYPES.LOCAL) ?
-
-
- { setPath(result) }}
- max={1024}
- format='all'
- onRemove={() => setPath('')}
- info={t('model.add.form.upload.info', { br: , max: 1024 })}
- >
-
- : null}
+
+ { setPath(result) }}
+ max={1024}
+ format='all'
+ onRemove={() => setPath('')}
+ info={t('model.add.form.upload.info', { br: , max: 1024 })}
+ >
+
+ : null}
{isType(TYPES.NET) ?
-
-
-
-
- : null}
-
-
-
+
-
-
-
-
-
-
- {t('common.action.import')}
-
-
-
- history.goBack()}>
- {t('task.btn.back')}
-
-
-
-
-
+ : null}
+
+
+
+
+
+ {t('common.action.import')}
+
+
+
+ history.goBack()}>
+ {t('task.btn.back')}
+
+
+
+
@@ -166,16 +170,4 @@ const Add = ({ importModel }) => {
)
}
-
-const actions = (dispatch) => {
- return {
- importModel: (payload) => {
- return dispatch({
- type: 'model/importModel',
- payload,
- })
- },
- }
-}
-
-export default connect(null, actions)(Add)
+export default Add
diff --git a/ymir/web/src/pages/model/detail.js b/ymir/web/src/pages/model/detail.js
index 4077b1c960..63ab04ac10 100644
--- a/ymir/web/src/pages/model/detail.js
+++ b/ymir/web/src/pages/model/detail.js
@@ -13,6 +13,10 @@ import TaskProgress from "@/components/task/progress"
import Error from "@/components/task/error"
import Hide from "@/components/common/hide"
import useRestore from "@/hooks/useRestore"
+import keywordsItem from "@/components/task/items/keywords"
+import { DescPop } from "../../components/common/descPop"
+import useRerunAction from "../../hooks/useRerunAction"
+import useCardTitle from '@/hooks/useCardTitle'
const { Item } = Descriptions
@@ -22,6 +26,8 @@ function ModelDetail({ modelCache, getModel }) {
const [model, setModel] = useState({ id })
const hideRef = useRef(null)
const restoreAction = useRestore(pid)
+ const generateRerunBtn = useRerunAction('btn')
+ const cardTitle = useCardTitle(model.name)
useEffect(async () => {
id && fetchModel(true)
@@ -39,16 +45,6 @@ function ModelDetail({ modelCache, getModel }) {
await getModel(id, force)
}
- function renderTitle() {
- return (
-
- {model.name} > {t(getTaskTypeLabel(model.taskType))}
- history.goBack()}>{t('common.back')}>
-
- )
- }
-
-
const hide = (version) => {
if (model?.project?.hiddenDatasets?.includes(version.id)) {
return message.warn(t('dataset.hide.single.invalid'))
@@ -67,31 +63,41 @@ function ModelDetail({ modelCache, getModel }) {
}
}
+ function getModelStage() {
+ const stage = model.recommendStage
+ return stage ? [id, stage].toString() : ''
+ }
+
return (
-
+
- {model.name} {model.versionName}
{model.hidden ? - {t('common.state.hidden')}
: null}
- {percent(model.map)}
+ {keywordsItem(model.keywords)}
+ -
+ {model.stages?.map(stage =>
{stage.name} mAP: {percent(stage.map)} )}
+
+
fetchModel(true)} />
- {model?.task?.error_code ? : null}
+
{!model.hidden ? <>
{model.url ? {t('model.action.download')} : null}
history.push(`/home/project/${model.projectId}/model/${model.id}/verify`)}>{t('model.action.verify')}
- history.push(`/home/task/mining/${model.projectId}?mid=${id}`)}>{t('dataset.action.mining')}
- history.push(`/home/task/inference/${model.projectId}?mid=${id}`)}>{t('dataset.action.inference')}
- history.push(`/home/task/train/${model.projectId}?mid=${id}`)}>{t('dataset.action.train')}
+ history.push(`/home/project/${model.projectId}/mining?mid=${getModelStage()}`)}>{t('dataset.action.mining')}
+ history.push(`/home/project/${model.projectId}/inference?mid=${getModelStage()}`)}>{t('dataset.action.inference')}
+ history.push(`/home/project/${model.projectId}/train?mid=${getModelStage()}`)}>{t('dataset.action.train')}
hide(model)}>{t('common.action.hide')}
> :
{t("common.action.restore")}
}
+ {generateRerunBtn(model)}
diff --git a/ymir/web/src/pages/model/verify.js b/ymir/web/src/pages/model/verify.js
index bba078c076..21ff197d54 100644
--- a/ymir/web/src/pages/model/verify.js
+++ b/ymir/web/src/pages/model/verify.js
@@ -11,13 +11,14 @@ import t from "@/utils/t"
import { format } from '@/utils/date'
import Breadcrumb from '@/components/common/breadcrumb'
import Uploader from "@/components/form/uploader"
-import AssetAnnotation from "@/components/dataset/asset_annotation"
+import AssetAnnotation from "@/components/dataset/assetAnnotation"
import { TYPES } from '@/constants/image'
import styles from './verify.less'
import { NavDatasetIcon, SearchEyeIcon, NoXlmxIcon } from '@/components/common/icons'
import ImgDef from '@/assets/img_def.png'
import ImageSelect from "@/components/form/imageSelect"
import { percent } from "@/utils/number"
+import useFetch from '@/hooks/useFetch'
const { CheckableTag } = Tag
@@ -25,10 +26,9 @@ const { CheckableTag } = Tag
const KeywordColor = ["green", "red", "cyan", "blue", "yellow", "purple", "magenta", "orange", "gold"]
-function Verify({ getModel, verify }) {
+function Verify({ verify }) {
const history = useHistory()
- const { mid: id } = useParams()
- const [model, setModel] = useState({})
+ const { mid: id, id: pid } = useParams()
const [url, setUrl] = useState('')
const [confidence, setConfidence] = useState(20)
const [annotations, setAnnotations] = useState([])
@@ -39,16 +39,14 @@ function Verify({ getModel, verify }) {
const [seniorConfig, setSeniorConfig] = useState([])
const [hpVisible, setHpVisible] = useState(false)
const IMGSIZELIMIT = 10
+ const [model, getModel] = useFetch('model/getModel', {})
- useEffect(async () => {
- const result = await getModel(id)
- if (result) {
- setModel(result)
- }
+ useEffect(() => {
+ getModel({ id })
}, [])
useEffect(() => {
- setShowAnnos(annotations.length ? annotations.filter(anno =>
+ setShowAnnos(annotations.length ? annotations.filter(anno =>
anno.score * 100 > confidence && selectedKeywords.indexOf(anno.keyword) > -1
) : [])
}, [confidence, annotations, selectedKeywords])
@@ -69,7 +67,6 @@ function Verify({ getModel, verify }) {
function urlChange(files, url) {
setUrl('')
setUrl(files.length ? url : '')
- // 上传图片后,清除上次的标注结果
setAnnotations([])
}
@@ -97,7 +94,6 @@ function Verify({ getModel, verify }) {
)
- // annotations.filter(anno => anno.confidence > confidence)
function renderUploadBtn(label = t('model.verify.upload.label')) {
return (
key && value ? config[key] = value : null)
// reinit annotations
setAnnotations([])
- const result = await verify(id, [url], image, config)
- // console.log('result: ', result)
+ const result = await verify({ projectId: pid, modelStage: [id, model.recommendStage], urls: [url], image, config })
if (result) {
- const all = result.annotations[0]?.detection || []
+ const all = result || []
setAnnotations(all)
if (all.length) {
@@ -159,9 +149,9 @@ function Verify({ getModel, verify }) {
}
const onFinish = () => {
- form.validateFields().then(() => {
- verifyImg()
- })
+ form.validateFields().then(() => {
+ verifyImg()
+ })
}
return (
@@ -169,7 +159,7 @@ function Verify({ getModel, verify }) {
-
+
{url ? (
) : renderUploader}
{url ? (
-
-
- `${value}%`} value={confidence} onChange={confidenceChange} />
-
-
-
-
+
+
+ `${value}%`} value={confidence} onChange={confidenceChange} />
+
+
+
+
) : null}
-
+
{t("model.verify.model.info.title")}>}
bordered={false}
@@ -227,84 +217,79 @@ function Verify({ getModel, verify }) {
+
{seniorConfig.length ?
- {t("model.verify.model.param.title")}>}
- bordered={false}
- style={{ marginRight: 20 }}
- headStyle={{ padding: 0, minHeight: 28 }}
- bodyStyle={{ padding: 0 }}
- extra={ setHpVisible(!hpVisible)}
- style={{ paddingLeft: 0 }}
- >{hpVisible ? t('model.verify.model.param.fold') : t('model.verify.model.param.unfold')}{hpVisible ? : }
- }
- >
- {t("model.verify.model.param.title")}>}
+ bordered={false}
+ style={{ marginRight: 20 }}
+ headStyle={{ padding: 0, minHeight: 28 }}
+ bodyStyle={{ padding: 0 }}
+ extra={ setHpVisible(!hpVisible)}
+ style={{ paddingLeft: 0 }}
+ >{hpVisible ? t('model.verify.model.param.fold') : t('model.verify.model.param.unfold')}{hpVisible ? : }
+ }
>
-
- {(fields, { add, remove }) => (
- <>
-
-
- {t('common.key')}
- {t('common.value')}
-
-
- >
- )}
-
-
- : null}
+ >
+ )}
+
+
+ : null}
{renderUploadBtn()}
- }
+ }
style={{ marginLeft: 20 }}
onClick={() => onFinish()}
- >
+ >
{t("breadcrumbs.model.verify")}
@@ -322,10 +307,10 @@ const actions = (dispatch) => ({
payload: { id, force },
})
},
- verify(id, urls, image, config) {
+ verify(payload) {
return dispatch({
type: 'model/verify',
- payload: { id, urls, image, config },
+ payload,
})
},
})
diff --git a/ymir/web/src/pages/model/verify.less b/ymir/web/src/pages/model/verify.less
index 8bbd8d8a0b..5c4e3ed13e 100644
--- a/ymir/web/src/pages/model/verify.less
+++ b/ymir/web/src/pages/model/verify.less
@@ -21,21 +21,10 @@
overflow-y: auto;
height: 100%;
margin-right: 20px;
- padding: 20px;
border: 1px solid rgba(0, 0, 0, 0.06);
text-align: center;
background: rgba(0, 0, 0, 0.02);
position: relative;
- &::-webkit-scrollbar {
- width: 1px;
- background-color: #ddd;
- }
- &::-webkit-scrollbar-thumb {
- width: 10px;
- border: 1px solid #0000ff10;
- background-color: #00000005;
- border-radius: 10px;
- }
.confidence {
height: 54px;
background: rgba(0, 0, 0, 0.4);
@@ -64,18 +53,7 @@
height: 100%;
display: flex;
flex-direction: column;
- overflow-y: auto;
overflow-x: hidden;
- &::-webkit-scrollbar {
- width: 1px;
- background-color: #ddd;
- }
- &::-webkit-scrollbar-thumb {
- width: 10px;
- border: 1px solid #0000ff10;
- background-color: #00000005;
- border-radius: 10px;
- }
.asset_form {
flex: 1;
:global(.ant-card-extra) {
diff --git a/ymir/web/src/pages/portal/datasetOrigin.js b/ymir/web/src/pages/portal/datasetOrigin.js
index 7a74a55f7d..25d89aecba 100644
--- a/ymir/web/src/pages/portal/datasetOrigin.js
+++ b/ymir/web/src/pages/portal/datasetOrigin.js
@@ -24,9 +24,7 @@ function Sets({ title, count = 2, getPublicDataset }) {
list.push(items.slice(r, r + count))
r += count
}
- // console.log('public dataset: ', result)
setSets(list)
- // setSets(result.items.slice(r, r + count))
}
}, [])
diff --git a/ymir/web/src/pages/portal/index.js b/ymir/web/src/pages/portal/index.js
index 6e409b8826..04874f7d66 100644
--- a/ymir/web/src/pages/portal/index.js
+++ b/ymir/web/src/pages/portal/index.js
@@ -4,7 +4,6 @@ import { Row, Col, } from "antd"
import ProjectChart from "./projectChart"
import MyProjects from './projectMy'
import PublicSets from './datasetOrigin'
-import ModelList from "./modelList"
import styles from "./index.less"
@@ -20,9 +19,6 @@ function Portal({ }) {
-
-
-
diff --git a/ymir/web/src/pages/portal/modelList.js b/ymir/web/src/pages/portal/modelList.js
deleted file mode 100644
index c5305d2677..0000000000
--- a/ymir/web/src/pages/portal/modelList.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Card, List, Space, Tag } from "antd"
-import { useEffect, useState } from "react"
-import { connect } from 'dva'
-import { Link } from "umi"
-
-import t from '@/utils/t'
-import styles from './index.less'
-import { cardBody, cardHead } from "./components/styles"
-import { OptimalModelIcon } from '@/components/common/icons'
-import Empty from '@/components/empty/default'
-import { percent } from "../../utils/number"
-
-const ModelList = ({ getModelsByMap }) => {
- const [keywords, setKeywords] = useState([])
- const [current, setCurrent] = useState('')
- const [kmodels, setKModels] = useState({})
-
- useEffect(() => {
- fetchModels()
- }, [])
-
- function changeKeyword(keyword) {
- setCurrent(keyword)
- }
-
- async function fetchModels() {
- const { keywords, kmodels } = await getModelsByMap(35)
- setKeywords(keywords)
- setKModels(kmodels)
- setCurrent(keywords[0])
- }
-
- return
{t('portal.model.best.title')} >}
- >
- {t('common.index.keyword.label')}: {keywords.map(k => changeKeyword(k)}>{k} )}
-
- {kmodels[current]?.length ? kmodels[current]?.filter(model => model).map((model, index) =>
- {percent(model.map)}]} title={model.name}>
-
- {index + 1}
- {model.name}
-
-
- ) :
- }
-
-
-}
-
-const actions = (dispatch) => {
- return {
- getModelsByMap(limit = 5) {
- return dispatch({
- type: 'model/getModelsByMap',
- payload: { limit },
- })
- },
- }
-}
-
-export default connect(null, actions)(ModelList)
diff --git a/ymir/web/src/pages/portal/projectChart.js b/ymir/web/src/pages/portal/projectChart.js
index f95dfe2bb6..f39116680d 100644
--- a/ymir/web/src/pages/portal/projectChart.js
+++ b/ymir/web/src/pages/portal/projectChart.js
@@ -44,9 +44,9 @@ const ProjectChart = ({ getProjectStats }) => {
useEffect(async () => {
const result = await getProjectStats(type)
- if (result && result.records) {
- const transData = transferData(result.records)
- setTimestamps(result.timestamps)
+ if (result?.length) {
+ const transData = transferData(result)
+ setTimestamps(result.map(item => item.legend))
setSeries(transData)
}
}, [type])
@@ -75,7 +75,7 @@ const ProjectChart = ({ getProjectStats }) => {
let result = [];
data.forEach(item => {
// filter
- result.push(item[1])
+ result.push(item.count)
})
const series = [{
name: t('portal.project'),
@@ -124,7 +124,6 @@ const ProjectChart = ({ getProjectStats }) => {
>
{series.length ? (
<>
- {console.log('option: ',option)}
>) :
}
diff --git a/ymir/web/src/pages/portal/projectMy.js b/ymir/web/src/pages/portal/projectMy.js
index d04dd8628d..1755abb8fc 100644
--- a/ymir/web/src/pages/portal/projectMy.js
+++ b/ymir/web/src/pages/portal/projectMy.js
@@ -37,24 +37,26 @@ const MyProject = ({ count = 6, ...func }) => {
return (
{t('portal.project.my.title')} >} link='/home/project'>
+ title={
+
+ {t('portal.project.my.title')}
+ >} link='/home/project'>
}
>
-
+
{t('portal.action.new.project')}
- {projects.length ?
:
-
+
addExample()}>
{t('project.new.example.label')}
- }
+ {projects.length ?
: null}
)
diff --git a/ymir/web/src/pages/project/add.js b/ymir/web/src/pages/project/add.js
index c94dfdc5a3..6d223415b9 100644
--- a/ymir/web/src/pages/project/add.js
+++ b/ymir/web/src/pages/project/add.js
@@ -1,53 +1,33 @@
-import { useEffect, useState } from 'react'
-import { Button, Card, Form, Input, message, Modal, Select, Space, Radio, Row, Col, InputNumber, ConfigProvider } from 'antd'
+import { useEffect, useCallback, useState } from 'react'
+import { Button, Card, Form, Input, message, Modal, Select, Space, Radio, Row, Col } from 'antd'
import { connect } from 'dva'
import { useParams, useHistory, useLocation } from "umi"
import s from './add.less'
import t from '@/utils/t'
-import { MiningStrategy } from '@/constants/project'
import Breadcrumbs from '@/components/common/breadcrumb'
-import Tip from '@/components/form/tip'
-import EmptyState from '@/components/empty/dataset'
-import DatasetSelect from '../../components/form/datasetSelect'
-import Panel from '../../components/form/panel'
+import DatasetSelect from '@/components/form/datasetSelect'
+import Panel from '@/components/form/panel'
+import useFetch from '@/hooks/useFetch'
const { useForm } = Form
const { confirm } = Modal
-const strategyOptions = Object.values(MiningStrategy)
- .filter(key => Number.isInteger(key))
- .map(value => ({
- value,
- label: t(`project.mining.strategy.${value}`),
- }))
-
-const Add = ({ keywords, datasets, projects, getProject, getKeywords, ...func }) => {
+const Add = ({ keywords, datasets, getKeywords, ...func }) => {
const { id } = useParams()
const history = useHistory()
const location = useLocation()
- const { settings } = location.query
const [form] = useForm()
- const [settingsVisible, setSettingsVisible] = useState(true)
const [isEdit, setEdit] = useState(false)
- const [project, setProject] = useState({ id })
-
- const [testSet, setTestSet] = useState(0)
- const [miningSet, setMiningSet] = useState(0)
- const [strategy, setStrategy] = useState(0)
+ const [project, getProject] = useFetch('project/getProject', {})
+ const [_, checkDuplication] = useFetch('keyword/checkDuplication')
useEffect(() => {
setEdit(!!id)
- id && fetchProject()
+ id && getProject({ id })
}, [id])
- useEffect(() => {
- if (projects[id]) {
- setProject(projects[id])
- }
- }, [projects[id]])
-
useEffect(() => {
getKeywords({ limit: 100000 })
}, [])
@@ -58,17 +38,14 @@ const Add = ({ keywords, datasets, projects, getProject, getKeywords, ...func })
function initForm(project = {}) {
const { name, keywords: kws, trainSetVersion,
- description, testSet: testDataset, miningSet: miningDataset, miningStrategy, chunkSize } = project
+ description, enableIteration, testingSets } = project
if (name) {
form.setFieldsValue({
name, keywords: kws, description,
trainSetVersion,
- testSet: testDataset?.id,
- miningSet: miningDataset?.id,
- strategy: miningStrategy || 0,
- chunkSize: miningStrategy === 0 && chunkSize ? chunkSize : undefined,
+ enableIteration,
+ testingSets: testingSets.length ? testingSets : undefined,
})
- setStrategy(miningStrategy)
}
}
@@ -76,14 +53,15 @@ const Add = ({ keywords, datasets, projects, getProject, getKeywords, ...func })
const action = isEdit ? 'update' : 'create'
var params = {
...values,
- chunkSize: strategy === 0 ? values.chunkSize : undefined,
}
- if (settings || isEdit) {
+ if (isEdit) {
params.id = id
}
- if (!settings) {
- params.name = (name || '').trim()
- params.description = (description || '').trim()
+ params.name = (name || '').trim()
+ params.description = (description || '').trim()
+
+ if (isEdit && params.name === project.name) {
+ delete params.name
}
const send = async () => {
@@ -91,7 +69,7 @@ const Add = ({ keywords, datasets, projects, getProject, getKeywords, ...func })
if (result) {
const pid = result.id || id
message.success(t(`project.${action}.success`))
- history.push(`/home/project/detail/${pid}`)
+ history.push(`/home/project/${pid}/detail`)
}
}
// edit project
@@ -100,16 +78,15 @@ const Add = ({ keywords, datasets, projects, getProject, getKeywords, ...func })
}
// create project
const kws = params.keywords.map(kw => (kw || '').trim()).filter(kw => kw)
- const { failed } = await func.checkKeywords(kws)
- const newKws = kws.filter(keyword => !failed.includes(keyword))
+ const { newer } = await checkDuplication(kws)
- if (newKws?.length) {
+ if (newer?.length) {
// confirm
confirm({
title: t('project.add.confirm.title'),
- content:
{newKws.map(keyword => {keyword} )} ,
+ content:
{newer.map(keyword => {keyword} )} ,
onOk: () => {
- addNewKeywords(newKws, send)
+ addNewKeywords(newer, send)
},
okText: t('project.add.confirm.ok'),
cancelText: t('project.add.confirm.cancel'),
@@ -127,9 +104,7 @@ const Add = ({ keywords, datasets, projects, getProject, getKeywords, ...func })
}
}
- async function fetchProject() {
- const result = await getProject(id)
- }
+ const testingFilter = useCallback(datasets => datasets.filter(ds => ds.keywordCount > 0 && ds.groupId !== project?.trainSet?.id), [project?.trainSet?.id])
function validateKeywords(_, kws) {
if (kws?.length) {
@@ -148,127 +123,94 @@ const Add = ({ keywords, datasets, projects, getProject, getKeywords, ...func })
-
-
-
-
- {isEdit ? t('common.confirm') : t('project.add.submit')}
-
-
-
- history.goBack()}>
- {t('common.back')}
-
-
-
-
+
+
@@ -280,7 +222,6 @@ const props = (state) => {
return {
keywords: state.keyword.keywords.items,
datasets: state.project.datasets,
- projects: state.project.projects,
}
}
@@ -309,12 +250,6 @@ const actions = (dispatch) => {
payload,
})
},
- getProject: (id) => {
- return dispatch({
- type: 'project/getProject',
- payload: { id },
- })
- },
getKeywords(payload) {
return dispatch({
type: 'keyword/getKeywords',
diff --git a/ymir/web/src/pages/project/components/datasets.js b/ymir/web/src/pages/project/components/datasets.js
deleted file mode 100644
index bee159d775..0000000000
--- a/ymir/web/src/pages/project/components/datasets.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { connect } from 'dva'
-
-import List from '@/components/dataset/list'
-
-function Datasets ({}) {
- return
-}
-
-const props = state => {
- return {
-
- }
-}
-
-const actions = dispacth => {
- return {
-
- }
-}
-
-export default connect(props, actions)(Datasets)
\ No newline at end of file
diff --git a/ymir/web/src/pages/project/components/detail.js b/ymir/web/src/pages/project/components/detail.js
new file mode 100644
index 0000000000..0044c49dab
--- /dev/null
+++ b/ymir/web/src/pages/project/components/detail.js
@@ -0,0 +1,68 @@
+import React, { useEffect } from "react"
+import { Button, Col, Popover, Row, Space, Tag } from "antd"
+import { Link, useSelector } from "umi"
+
+import t from "@/utils/t"
+import { getStageLabel } from '@/constants/iteration'
+import useFetch from '@/hooks/useFetch'
+
+import s from "../detail.less"
+import SampleRates from "@/components/dataset/sampleRates"
+import { TestingSet } from "./testingSet"
+import { EditIcon, SearchEyeIcon, EyeOffIcon } from "@/components/common/icons"
+import { ArrowDownIcon, ArrowRightIcon } from '@/components/common/icons'
+
+function ProjectDetail({ project = {} }) {
+ const id = project.id
+
+ const unfold = useSelector(({ iteration }) => iteration.actionPanelExpand)
+ const [_, toggleActionPanel] = useFetch('iteration/toggleActionPanel', true)
+
+ function renderProjectDatasetLabel() {
+ const getDsName = (ds = {}) => ds.name ? (ds.name + ' ' + (ds.versionName || '')) : ''
+ const maps = [
+ { label: 'project.add.form.training.set', name: getDsName(project.trainSet) },
+ { dataset: project.testSet, label: 'project.add.form.test.set', name: getDsName(project.testSet) },
+ { dataset: project.miningSet, label: 'project.add.form.mining.set', name: getDsName(project.miningSet) },
+ ]
+
+ return maps.map(({ name, label, dataset }) => {
+ return
+
{t(label)}: {dataset ? renderPop(name, dataset) : name}
+
+ })
+ }
+
+ function renderPop(label, dataset = {}) {
+ dataset.project = project
+ const content =
+ return
+ {label}
+
+ }
+
+ return
+
+
+
+ {project.name}
+
+ {t('project.detail.info.iteration', {
+ stageLabel: {t(getStageLabel(project.currentStage, project.round))} ,
+ current: {project.round} ,
+ })}
+
+ {t('project.train_classes')}: {project?.keywords?.join(',')}
+ {project.description ? {t('project.detail.desc')}: {project.description} : null}
+
+
+
+ {t('project.iteration.settings.title')}
+ toggleActionPanel(!unfold)}>
+ {unfold ? <> {t(`iteration.fold`)}> : <> {t(`iteration.unfold`)}>}
+
+
+
+
+}
+export default ProjectDetail
diff --git a/ymir/web/src/pages/project/components/hiddenList.js b/ymir/web/src/pages/project/components/hiddenList.js
index 6d32bd2f40..6548c40756 100644
--- a/ymir/web/src/pages/project/components/hiddenList.js
+++ b/ymir/web/src/pages/project/components/hiddenList.js
@@ -1,15 +1,18 @@
import React, { useEffect, useState } from "react"
import { connect } from 'dva'
-import s from "../index.less"
-import { Table, Space, Button, message, } from "antd"
+import { Table, Space, Button, message, Card, } from "antd"
+import { useHistory } from "umi"
import t from "@/utils/t"
import { humanize } from "@/utils/number"
+import { tabs } from '@/constants/project'
import Actions from "@/components/table/actions"
+import s from "../index.less"
import { EyeOnIcon } from "@/components/common/icons"
import useRestore from "@/hooks/useRestore"
const HiddenList = ({ module, pid, ...func }) => {
+ const history = useHistory()
const [list, setHiddenList] = useState([])
const [total, setTotal] = useState(0)
const [query, setQuery] = useState(null)
@@ -26,6 +29,10 @@ const HiddenList = ({ module, pid, ...func }) => {
query && fetch()
}, [query])
+ function tabChange(key) {
+ history.push(`#${key}`)
+ }
+
const columns = [
{
title: showTitle("dataset.column.name"),
@@ -33,10 +40,13 @@ const HiddenList = ({ module, pid, ...func }) => {
render: (name, { versionName }) => `${name} ${versionName}`,
ellipsis: { showTitle: true },
},
- {
+ module === 'dataset' ? {
title: showTitle("dataset.column.asset_count"),
dataIndex: "assetCount",
render: (num) => humanize(num),
+ } : {
+ title: showTitle("model.column.map"),
+ dataIndex: "map",
},
{
title: showTitle("dataset.column.hidden_time"),
@@ -114,26 +124,33 @@ const HiddenList = ({ module, pid, ...func }) => {
{t("common.action.multiple.restore")}
-
-
record.id}
- rowSelection={{
- selectedRowKeys: selected,
- onChange: (keys) => rowSelectChange(keys),
- }}
- rowClassName={(record, index) => index % 2 === 0 ? '' : 'oddRow'}
- columns={columns}
- >
-
+
+
({ ...tab, tab: t(tab.tab) }))} activeTabKey={module} onTabChange={tabChange}
+ className='noShadow'
+ bordered={false}
+ style={{ margin: '-20px -20px 0', background: 'transparent' }}
+ headStyle={{ padding: '0 20px', background: '#fff', marginBottom: '20px' }}
+ bodyStyle={{ padding: '0 20px' }}>
+ record.id}
+ rowSelection={{
+ selectedRowKeys: selected,
+ onChange: (keys) => rowSelectChange(keys),
+ }}
+ rowClassName={(record, index) => index % 2 === 0 ? '' : 'oddRow'}
+ columns={columns}
+ >
+
+
}
diff --git a/ymir/web/src/pages/project/components/iteration.js b/ymir/web/src/pages/project/components/iteration.js
deleted file mode 100644
index d810ecb855..0000000000
--- a/ymir/web/src/pages/project/components/iteration.js
+++ /dev/null
@@ -1,201 +0,0 @@
-import { useCallback, useEffect, useState } from "react"
-import { Row, Col } from "antd"
-import { connect } from "dva"
-
-import { Stages, StageList } from '@/constants/project'
-import { templateString } from '@/utils/string'
-import Stage from './stage'
-import s from "./iteration.less"
-
-function Iteration({ project, fresh = () => { }, ...func }) {
- const [iteration, setIteration] = useState({})
- const [stages, setStages] = useState([])
- const [prevIteration, setPrevIteration] = useState({})
-
- useEffect(() => {
- initStages()
- }, [project])
-
- useEffect(() => {
- if (project.id && project.currentIteration) {
- fetchStagesResult(project.currentIteration)
- }
- }, [project.currentIteration])
-
- useEffect(() => {
- if (iteration.prevIteration) {
- fetchPrevIteration()
- }
- }, [iteration])
-
- useEffect(() => {
- iteration.id && rerenderStages()
- }, [iteration, prevIteration])
-
- const callback = useCallback(iterationHandle, [iteration])
-
- function initStages() {
- const stageList = StageList()
- const ss = stageList.list.map(({ label, value, url, output, input }) => {
- const slabel = `project.iteration.stage.${label}`
- return {
- value: value,
- label,
- act: slabel,
- react: `${slabel}.react`,
- state: -1,
- next: stageList[value].next,
- temp: url,
- output,
- input,
- project,
- unskippable: [Stages.merging, Stages.training].includes(value),
- callback,
- }
- })
-
- setStages(ss)
- }
-
- function rerenderStages() {
- const ss = stages.map(stage => {
- const result = iteration[`i${stage.output}`] || iteration[stage.output]
- const urlParams = {
- s0d: project.miningSet.id || 0,
- s0s: project.miningStrategy,
- s0c: project.chunkSize || undefined,
- s1d: iteration.miningSet,
- s1m: prevIteration.model || project.model,
- s2d: iteration.miningResult,
- s3d: prevIteration.trainUpdateSet || project.trainSetVersion,
- s3m: iteration.labelSet,
- s4d: iteration.trainUpdateSet,
- s4t: iteration.testSet,
- id: iteration.id,
- pid: project.id,
- stage: iteration.currentStage,
- output: stage.output,
- }
- const url = templateString(stage.temp || '', urlParams)
- return {
- ...stage,
- iterationId: iteration.id,
- round: iteration.round,
- current: iteration.currentStage,
- url,
- result,
- }
- })
- setStages(ss)
- }
-
- function iterationHandle({ type = 'update', data = {} }) {
- if (type === 'create') {
- createIteration(data)
- } else if (type === 'skip') {
- skipStage(data)
- } else {
- updateIteration(data)
- }
- }
-
- async function fetchStagesResult(iteration) {
- const iterationWithResult = await func.getIterationStagesResult(iteration)
- setIteration(iterationWithResult)
- }
-
- async function fetchPrevIteration() {
- const result = await func.getIteration(project.id, iteration.prevIteration)
- if (result) {
- setPrevIteration(result)
- }
- }
-
- async function createIteration(data = {}) {
- const params = {
- iterationRound: data.round,
- projectId: project.id,
- prevIteration: iteration.id,
- testSet: project.testSet.id,
- }
- const result = await func.createIteration(params)
- if (result) {
- fresh()
- }
- }
- async function updateIteration(data = {}) {
- const params = {
- id: iteration.id,
- currentStage: data.stage.value,
- }
- const result = await func.updateIteration(params)
- if (result) {
- fetchStagesResult(result)
- // setIteration(result)
- }
- }
- async function skipStage({ stage = {} }) {
- const params = {
- id: iteration.id,
- currentStage: stage.value,
- [stage.input]: 0,
- }
- const result = await func.updateIteration(params)
- if (result) {
- fetchStagesResult(result)
- // setIteration(result)
- }
- }
- return (
-
-
- {stages.map((stage) => (
-
-
-
- ))}
-
-
- )
-}
-
-const props = (state) => {
- return {}
-}
-
-const actions = (dispacth) => {
- return {
- getIteration(pid, id) {
- return dispacth({
- type: 'iteration/getIteration',
- payload: { pid, id },
- })
- },
- getIterationStagesResult(iteration) {
- return dispacth({
- type: 'iteration/getIterationStagesResult',
- payload: iteration,
- })
- },
- updateIteration(params) {
- return dispacth({
- type: 'iteration/updateIteration',
- payload: params,
- })
- },
- createIteration(params) {
- return dispacth({
- type: 'iteration/createIteration',
- payload: params,
- })
- },
- queryFirstTrainDataset(group_id) {
- return dispacth({
- type: 'dataset/queryDatasets',
- payload: { group_id, is_desc: false, limit: 1 }
- })
- }
- }
-}
-
-export default connect(props, actions)(Iteration)
diff --git a/ymir/web/src/pages/project/components/list.hoc.js b/ymir/web/src/pages/project/components/list.hoc.js
new file mode 100644
index 0000000000..4ef73407d9
--- /dev/null
+++ b/ymir/web/src/pages/project/components/list.hoc.js
@@ -0,0 +1,56 @@
+import React, { useCallback, useEffect, useState } from "react"
+import { Card } from "antd"
+import { useLocation, useParams } from "umi"
+
+import useFetch from '@/hooks/useFetch'
+import Breadcrumbs from "@/components/common/breadcrumb"
+// import Detail from './detail'
+import NoIterationDetail from "./noIterationDetail"
+
+import s from "../detail.less"
+
+const ListHOC = (Module) => {
+
+ function List() {
+ const location = useLocation()
+ const { id } = useParams()
+ const [iterations, getIterations] = useFetch('iteration/getIterations', [])
+ const [groups, setGroups] = useState([])
+ const [project, getProject, setProject] = useFetch('project/getProject', {})
+
+ useEffect(() => {
+ id && getProject({ id, force: true })
+ id && getIterations({ id })
+ }, [id])
+
+ useEffect(() => {
+ const gids = location.hash.replace(/^#/, '')
+ if (gids) {
+ setGroups(gids.split(','))
+ }
+ }, [location.hash])
+
+ const fresh = useCallback(project => {
+ if (project) {
+ setProject(project)
+ } else {
+ getProject({ id, force: true })
+ }
+ }, [id])
+
+ return (
+
+
+
+
+
+
+ )
+ }
+ return List
+}
+
+export default ListHOC
diff --git a/ymir/web/src/pages/project/components/list.js b/ymir/web/src/pages/project/components/list.js
index 34d391f5ed..806d9dfe43 100644
--- a/ymir/web/src/pages/project/components/list.js
+++ b/ymir/web/src/pages/project/components/list.js
@@ -5,11 +5,12 @@ import { useHistory, Link } from "umi"
import { List, Skeleton, Space, Pagination, Col, Row, Card, Button, Form, Input, message, ConfigProvider, } from "antd"
import t from "@/utils/t"
-import { getStageLabel } from '@/constants/project'
+import { getStageLabel } from '@/constants/iteration'
import ProjectEmpty from '@/components/empty/project'
import Del from './del'
import s from "./list.less"
import { EditIcon, DeleteIcon, AddIcon, SearchIcon } from "@/components/common/icons"
+import KeywordsItem from "@/components/project/keywordsItem"
const ProjectList = ({ list, query, ...func }) => {
@@ -38,7 +39,7 @@ const ProjectList = ({ list, query, ...func }) => {
const pageChange = (current, pageSize) => {
const limit = pageSize
const offset = (current - 1) * pageSize
- func.updateQuery({ ...query, limit, offset })
+ func.updateQuery({ ...query, current, limit, offset })
}
async function getData() {
@@ -54,7 +55,7 @@ const ProjectList = ({ list, query, ...func }) => {
label: t("project.action.edit"),
onclick: (e) => {
e.stopPropagation()
- history.push(`/home/project/add/${id}`)
+ history.push(`/home/project/${id}/add`)
},
icon:
,
},
@@ -97,7 +98,7 @@ const ProjectList = ({ list, query, ...func }) => {
getData()
}
}
-
+
async function initState() {
await func.resetQuery()
form.resetFields()
@@ -146,15 +147,15 @@ const ProjectList = ({ list, query, ...func }) => {
const title =
- {item.name}
+ {item.name} {item.isExample ? {t('project.example')} : null}
{t('project.train_classes')}:
- {item.keywords.join(',')}
+
-
+ {item.enableIteration ?
{t('project.iteration.current')}:
{t(getStageLabel(item.currentStage, item.round))}
-
+ : null}
{more(item)}
@@ -175,12 +176,14 @@ const ProjectList = ({ list, query, ...func }) => {
{item.trainSet?.name} |
{item.testSet?.name} |
{item.miningSet?.name}
-
+
-
+ {item.enableIteration ?
{t('project.iteration.number')}
- {item.round}
-
+
+ {item.round}
+
+ : null}
{t('project.content.desc')}: {item.description}
@@ -190,7 +193,7 @@ const ProjectList = ({ list, query, ...func }) => {
return
- history.push(`/home/project/detail/${item.id}`)}>
+ history.push(`/home/project/${item.id}/detail`)}>
@@ -208,8 +211,8 @@ const ProjectList = ({ list, query, ...func }) => {
renderItem={renderItem}
/>
- t('project.list.total', { total })}
showQuickJumper showSizeChanger />
diff --git a/ymir/web/src/pages/project/components/noIterationDetail.js b/ymir/web/src/pages/project/components/noIterationDetail.js
new file mode 100644
index 0000000000..3ba0c25d7d
--- /dev/null
+++ b/ymir/web/src/pages/project/components/noIterationDetail.js
@@ -0,0 +1,21 @@
+import { Col, Row, Space } from "antd"
+import { Link } from "umi"
+import t from "@/utils/t"
+import s from "../detail.less"
+import { EditIcon, EyeOffIcon } from "@/components/common/icons"
+import { TestingSet } from "./testingSet"
+
+const NoIterationDetail = ({ project }) => {
+ return (
+
+
+ {project.name}
+ {t('project.train_classes')}: {project?.keywords?.join(',')}
+ {project.description ? {t('project.detail.desc')}: {project.description} : null}
+
+
+
+ )
+}
+
+export default NoIterationDetail
diff --git a/ymir/web/src/pages/project/components/prepare.js b/ymir/web/src/pages/project/components/prepare.js
deleted file mode 100644
index 2437f86095..0000000000
--- a/ymir/web/src/pages/project/components/prepare.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import { useCallback, useEffect, useState } from "react"
-import { Row, Col } from "antd"
-import { connect } from "dva"
-
-import { states } from '@/constants/dataset'
-import { Stages, StageList } from '@/constants/project'
-import Stage from './stage'
-import s from "./iteration.less"
-
-function Prepare({ project = {}, fresh = () => { }, ...func }) {
- const [stages, setStages] = useState([])
-
- useEffect(() => {
- project.id && initStages()
- }, [project])
-
- const validNext = () => project.miningSet && project.testSet && project.model
-
- function initStages() {
- const labels = [
- { value: 'datasets', state: project.miningSet && project.testSet ? states.VALID : -1, url: `/home/project/add/${project.id}?settings=1`, },
- { value: 'model', state: project.model ? states.VALID : -1, url: `/home/project/initmodel/${project.id}`, },
- { value: 'start', state: validNext() ? states.VALID : -1, },
- ]
- const ss = labels.map(({ value, state, url, }, index) => {
- const act = `project.iteration.stage.${value}`
- const stage = {
- value: index,
- label: value,
- act,
- // react: `${act}.react`,
- // next: index + 2,
- url,
- state,
- current: index,
- unskippable: true,
- callback: fresh,
- }
- if (index === labels.length - 1) {
- stage.react = ''
- stage.current = validNext() ? index : 0
- stage.callback = () => createIteration()
- console.log('project:', project, stage)
- }
- return stage
- })
- setStages(ss)
- }
-
- async function createIteration() {
- const params = {
- iterationRound: 1,
- projectId: project.id,
- prevIteration: 0,
- testSet: project?.testSet?.id,
- }
- const result = await func.createIteration(params)
- if (result) {
- fresh()
- }
- }
-
- return (
-
-
- {stages.map((stage, index) => (
- = stages.length - 1 ? null : 1}>
- = stages.length - 1} callback={stage.callback} />
-
- ))}
-
-
- )
-}
-
-const props = (state) => {
- return {}
-}
-
-const actions = (dispacth) => {
- return {
- createIteration(params) {
- return dispacth({
- type: 'iteration/createIteration',
- payload: params,
- })
- },
- queryTrainDataset(group_id) {
- return dispacth({
- type: 'dataset/queryDatasets',
- payload: { group_id, is_desc: false, limit: 1 }
- })
- }
- }
-}
-
-export default connect(props, actions)(Prepare)
diff --git a/ymir/web/src/pages/project/components/stage.js b/ymir/web/src/pages/project/components/stage.js
deleted file mode 100644
index e69b16af4e..0000000000
--- a/ymir/web/src/pages/project/components/stage.js
+++ /dev/null
@@ -1,180 +0,0 @@
-import { Button, Col, Popover, Row, Space } from "antd"
-import { useHistory, connect } from "umi"
-
-import t from '@/utils/t'
-import { states, statesLabel } from '@/constants/dataset'
-import s from './iteration.less'
-import { useEffect, useState } from "react"
-import RenderProgress from "../../../components/common/progress"
-import { YesIcon } from '@/components/common/icons'
-
-function Stage({ pid, stage, stageResult, current = 0, end = false, callback = () => { }, ...func }) {
- const history = useHistory()
- const [result, setResult] = useState({})
- const [state, setState] = useState(-1)
-
- useEffect(() => {
- const st = typeof result.state !== 'undefined' ? result.state : stage.state
- setState(st)
- }, [result, stage])
-
- useEffect(() => {
- currentStage() && func.setCurrentStageResult(stage.result)
- }, [stage.result])
-
- useEffect(() => {
- const res = currentStage() && stageResult?.id === stage?.result?.id ? stageResult : stage.result
- setResult(res || {})
- }, [stage.result, stageResult])
-
- useEffect(() => {
- if (stageResult?.id !== stage.result?.id) {
- return
- }
- if (stageResult.needReload) {
- fetchStageResult(true)
- }
- }, [stageResult])
-
- function skip() {
- callback({
- type: 'skip',
- data: { stage: stage.next },
- })
- }
-
- function next() {
- if (isValid()) {
- callback({
- type: 'update',
- data: { stage: stage.next },
- })
- } else {
- act()
- }
- }
-
- function ending() {
- if (end) {
- callback({
- type: 'create',
- data: {
- round: stage.round + 1,
- },
- })
- } else {
- act()
- }
- }
-
- const currentStage = () => stage.value === stage.current
- const finishStage = () => stage.value < stage.current
- const pendingStage = () => stage.value > stage.current
-
- const isPending = () => state < 0
- const isReady = () => state === states.READY
- const isValid = () => state === states.VALID
- const isInvalid = () => state === states.INVALID
-
- function act() {
- stage.url && history.push(stage.url)
- }
-
- async function fetchStageResult(force) {
- await func.getStageResult(stage.result?.id, stage.current, force)
- }
-
- const stateClass = `${s.stage} ${currentStage() ? s.current : (finishStage() ? s.finish : s.pending)}`
-
- const renderCount = () => {
- const content = finishStage() || (currentStage() && isValid()) ? : stage.value + 1
- const cls = pendingStage() ? s.pending : (currentStage() ? s.current : s.finish)
- return {content}
- }
- const renderMain = () => {
- return currentStage() ? renderMainBtn() : {t(stage.act)}
- }
-
- const renderMainBtn = () => {
- // show by task state and result
- const content = RenderProgress(result.state, result, true)
- const disabled = isReady() || isInvalid()
- const label = isValid() && stage.next ? t('common.step.next') : t(stage.act)
- const btn = stage.next ? next() : ending()}>{label}
- const pop = {btn}
- return result.id ? pop : btn
- }
-
- const renderReactBtn = () => {
- return stage.react && currentStage()
- && (isInvalid() || isValid())
- ? act()}>{t(stage.react)}
- : null
- }
- const renderState = () => {
- const pending = 'project.stage.state.pending'
- return !pendingStage() ?
- (isValid() ?
- (result.name ?`${result.name} ${result.versionName}` :
- (end ? null : t('common.done'))) :
- {isPending() && currentStage() ? t('project.stage.state.pending.current') : t(statesLabel(state))} ) :
- {t(pending)}
- }
-
- const renderSkip = () => {
- return !stage.unskippable && !end && currentStage() ? skip()}>{t('common.skip')} : null
- }
- return (
-
-
- {renderCount()}
-
-
- {renderMain()}
- {renderReactBtn()}
-
-
- {!end ? : null}
-
-
-
-
- {renderState()}
-
- {renderSkip()}
-
-
- )
-}
-
-const props = (state) => {
- return {
- userId: state.user.id,
- stageResult: state.iteration.currentStageResult,
- }
-}
-
-const actions = (dispacth) => {
- return {
- getStageResult(id, stage, force) {
- return dispacth({
- type: 'iteration/getStageResult',
- payload: { id, stage, force },
- })
- },
- setCurrentStageResult(result) {
- return dispacth({
- type: 'iteration/setCurrentStageResult',
- payload: result,
- })
- },
- createIteration(params) {
- return dispacth({
- type: 'iteration/createIteration',
- payload: params,
- })
- }
- }
-}
-
-export default connect(props, actions)(Stage)
diff --git a/ymir/web/src/pages/project/components/testingSet.js b/ymir/web/src/pages/project/components/testingSet.js
new file mode 100644
index 0000000000..756b9f033f
--- /dev/null
+++ b/ymir/web/src/pages/project/components/testingSet.js
@@ -0,0 +1,56 @@
+import React, { useEffect } from "react"
+import useFetch from "@/hooks/useFetch"
+import { Col, Popover, Row, Tag } from "antd"
+import SampleRates from "@/components/dataset/sampleRates"
+import t from "@/utils/t"
+import s from "../detail.less"
+
+export const TestingSet = ({ project }) => {
+ const [datasets, fetchDatasets] = useFetch('dataset/batchDatasets', [])
+
+ useEffect(() => {
+ project?.testingSets?.length && fetchDatasets({ pid: project.id, ids: project.testingSets })
+ }, [project.testingSets])
+
+ function renderProjectTestingSetLabel() {
+ const getDsName = (ds = {}) => ds.name ? (ds.name + ' ' + (ds.versionName || '')) : ''
+ const getAssetCount = (ds = {}) => ds.assetCount ? ds.assetCount : ''
+ const getDatasetGroup = (dsg = []) => {
+ return dsg.map(ds => {
+ return {
+ name: getDsName(ds),
+ assetCount: getAssetCount(ds),
+ dataset: ds
+ }
+ })
+ }
+ const maps = [
+ { label: 'project.add.form.testing.set', datasetGroup: getDatasetGroup(datasets) }
+ ]
+
+ return maps.map(({ label, datasetGroup }) => {
+ return
+
+ {t(label)}:
+
+
+ {datasetGroup.map(({ name, dataset, assetCount }) => {
+ const rlabel = name ? {name}{assetCount ? `(${assetCount})` : ''} : ''
+ return {dataset ? renderPop(rlabel, dataset) : rlabel}
+ })}
+
+
+
+ })
+ }
+
+ function renderPop(label, dataset = {}) {
+ dataset.project = project
+ const content =
+ return
+ {label}
+
+ }
+
+ return datasets.length ? renderProjectTestingSetLabel() : null
+}
diff --git a/ymir/web/src/pages/project/dataset.js b/ymir/web/src/pages/project/dataset.js
new file mode 100644
index 0000000000..edf0fa0b81
--- /dev/null
+++ b/ymir/web/src/pages/project/dataset.js
@@ -0,0 +1,6 @@
+import Datasets from '@/components/dataset/list'
+import ListHOC from "./components/list.hoc"
+
+const List = ListHOC(Datasets)
+
+export default () =>
diff --git a/ymir/web/src/pages/project/detail.js b/ymir/web/src/pages/project/detail.js
index b47e6a4761..9fa9d228f5 100644
--- a/ymir/web/src/pages/project/detail.js
+++ b/ymir/web/src/pages/project/detail.js
@@ -1,150 +1,99 @@
-import React, { useCallback, useEffect, useState } from "react"
-import { Card, Col, Popover, Row, Space } from "antd"
-import { useLocation, useParams, connect, Link, useHistory } from "umi"
+import React, { useEffect } from "react"
+import { Button, Card, Col, Row, Space } from "antd"
+import { useParams, useHistory } from "umi"
import t from "@/utils/t"
-import { getStageLabel, tabs } from '@/constants/project'
+import useFetch from '@/hooks/useFetch'
import Breadcrumbs from "@/components/common/breadcrumb"
-import Iteration from './components/iteration'
-import Datasets from '@/components/dataset/list'
-import Models from '@/components/model/list'
+import Empty from "@/components/empty/default"
+import { getStageLabel } from '@/constants/iteration'
import s from "./detail.less"
-import Prepare from "./components/prepare"
-import KeywordRates from "@/components/dataset/keywordRates"
-import { EditIcon, SearchEyeIcon, EyeOffIcon } from "../../components/common/icons"
-import CheckProjectDirty from "../../components/common/CheckProjectDirty"
+import { TrainIcon, NavDatasetIcon, ArrowRightIcon, ImportIcon } from "@/components/common/icons"
function ProjectDetail(func) {
const history = useHistory()
- const location = useLocation()
const { id } = useParams()
- const [iterations, setIterations] = useState([])
- const [group, setGroup] = useState(0)
- const [project, setProject] = useState({})
- const [active, setActive] = useState(tabs[0].key)
- const content = {
- [tabs[0].key]: ,
- [tabs[1].key]:
- }
+ const [project, getProject] = useFetch('project/getProject', {})
useEffect(() => {
- id && fetchProject(true)
- id && fetchIterations(id)
+ id && getProject({ id, force: true })
}, [id])
- useEffect(() => {
- const locationHash = location.hash.replace(/^#/, '')
- const [tabKey, gid] = (locationHash || '').split('_')
- setGroup(gid)
- setActive(tabKey || tabs[0].key)
- }, [location.hash])
+ const title = (Icon, label) =>
+
+ {t(label)}
+
- async function fetchProject(force) {
- const result = await func.getProject(id, force)
- if (result) {
- setProject(result)
- }
+ function add() {
+ history.push(`/home/project/${id}/dataset/add`)
}
- const fresh = useCallback(() => {
- fetchProject(true)
- }, [])
- function tabChange(key) {
- history.push(`#${key}`)
- }
-
- async function fetchIterations(pid) {
- const iterations = await func.getIterations(pid)
- if (iterations) {
- setIterations(iterations)
- }
- }
-
- function renderProjectDatasetLabel() {
- const getDsName = (ds = {}) => ds.name ? (ds.name + ' ' + (ds.versionName || '')) : ''
- const maps = [
- { label: 'project.add.form.training.set', name: getDsName(project.trainSet) },
- { dataset: project.testSet, label: 'project.add.form.test.set', name: getDsName(project.testSet) },
- { dataset: project.miningSet, label: 'project.add.form.mining.set', name: getDsName(project.miningSet) },
- ]
-
- return maps.map(({ name, label, dataset }) => {
- const rlabel = {t(label)}: {name}
- return
- {dataset ? renderPop(rlabel, dataset) : rlabel}
-
- })
- }
-
-
- function renderPop(label, dataset = {}) {
- dataset.project = project
- const content =
- return
- {label}
-
+ function goTraining() {
+ history.push(`/home/project/${id}/train`)
}
return (
-
+
-
-
-
- {project.name}
-
- {t('project.detail.info.iteration', {
- stageLabel: {t(getStageLabel(project.currentStage, project.round))} ,
- current: {project.round} ,
- })}
-
- {t('project.train_classes')}: {project?.keywords?.join(',')}
- {project.description ? {t('project.detail.desc')}: {project.description} : null}
-
+ {project.round > 0 ?
+ {t('project.iteration.entrance.status', {
+ stateLabel: {t(getStageLabel(project.currentStage, project.round))}
+ })}
+ history.push(`/home/project/${id}/iterations`)}> {t('project.iteration.entrance.btn')}
+
:
+
+
+
{t('project.iteration.entrance.empty.info')}
+
history.push(`/home/project/${id}/iterations`)}>
+ {t('project.iteration.entrance.empty.btn')}
+
+
}
+
+
+ {t("dataset.import.label")}
+ {t("project.iteration.stage.training")}
+
+
+
+
+ { history.push(`/home/project/${project.id}/dataset`) }}
+ extra={ }>
+
+
+ {t('project.tab.set.title')}
+ {project.setCount}
+
+
+ {t('project.detail.datavolume')}
+ {project.totalAssetCount}
+
+
+
-
-
- {t('project.settings.title')}
- {t('breadcrumbs.project.iterations')}
- {t('common.hidden.list')}
-
+
+ { history.push(`/home/project/${project.id}/model`) }}
+ extra={ }>
+
+
+ {t('project.tab.model.title')}
+ {project.modelCount}
+
+
+ {t('project.detail.runningtasks')}/{t('project.detail.totaltasks')}
+ {project.runningTaskCount}/{project.totalTaskCount}
+
+
+
- {project.round > 0 ?
-
:
}
-
- {renderProjectDatasetLabel()}
-
+
-
({ ...tab, tab: t(tab.tab) }))} tabBarExtraContent={ }
- activeTabKey={active} onTabChange={tabChange} className='noShadow'
- style={{ margin: '-20px -5vw 0', background: 'transparent' }}
- headStyle={{ padding: '0 5vw', background: '#fff', marginBottom: '10px' }}
- bodyStyle={{ padding: '0 5vw' }}>
- {content[active]}
-
)
}
-
-const actions = (dispatch) => {
- return {
- getProject(id, force) {
- return dispatch({
- type: 'project/getProject',
- payload: { id, force },
- })
- },
- getIterations(id) {
- return dispatch({
- type: 'iteration/getIterations',
- payload: { id, },
- })
- },
- }
-}
-
-export default connect(null, actions)(ProjectDetail)
+export default ProjectDetail
diff --git a/ymir/web/src/pages/project/detail.less b/ymir/web/src/pages/project/detail.less
index fd9a5847b3..8ecb624e65 100644
--- a/ymir/web/src/pages/project/detail.less
+++ b/ymir/web/src/pages/project/detail.less
@@ -1,7 +1,8 @@
.header {
background: #fff;
- margin: -20px -5vw 0;
- padding: 0 5vw 30px;
+ margin: 0 0 20px;
+ padding: 10px 20px 20px;
+ border-radius: 5px;
}
.detailPanel {
margin-bottom: 20px;
@@ -15,10 +16,7 @@
box-shadow: 0px 0px 0px rgb(255 255 255);
}
.detailPanel {
- :global(.ant-space-item) {
- margin-right: 16px;
- color: rgba(0, 0, 0, 0.65);
- }
+ color: rgba(0, 0, 0, 0.65);
.name {
font-size: 20px;
font-weight: bold;
@@ -47,3 +45,51 @@
overflow: hidden;
white-space: nowrap;
}
+
+.datasetTitle {
+ margin-right: 10px;
+ font-size: 14px;
+ color: rgba(0, 0, 0, 0.85);
+}
+
+.detailContainer .nameTag {
+ background: rgba(0, 0, 0, 0.06);
+ border-radius: 2px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+}
+.projectOverview {
+ background: #fff;
+ min-height: calc(100vh - 384px);
+ margin: 10px 0;
+ padding: 20px;
+ .cardContainer {
+ cursor: pointer;
+ }
+ .cardTitle {
+ font-size: 18px;
+ font-weight: bold;
+ color: rgba(0, 0, 0, 0.85);
+ display: flex;
+ align-items: center;
+ .titleIcon {
+ font-size: 24px;
+ }
+ .titleLabel {
+ margin-left: 6px;
+ }
+ }
+ .rightIcon {
+ color: rgba(0, 0, 0, 0.45);
+ }
+ .num {
+ height: 44px;
+ font-size: 28px;
+ color: rgba(0, 0, 0, 0.85);
+ }
+ .blue {
+ color: #2CBDE9;
+ }
+ .red {
+ color: #F2637B;
+ }
+}
\ No newline at end of file
diff --git a/ymir/web/src/pages/project/diagnose.js b/ymir/web/src/pages/project/diagnose.js
new file mode 100644
index 0000000000..3c8c79d167
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose.js
@@ -0,0 +1,56 @@
+import React, { useEffect, useState } from "react"
+import { Card } from "antd"
+import { useLocation, useParams, useHistory } from "umi"
+
+import t from "@/utils/t"
+import useFetch from "@/hooks/useFetch"
+import Breadcrumbs from "@/components/common/breadcrumb"
+
+import Metrics from "./diagnose/metrics"
+import Training from "./diagnose/training"
+
+import s from "./detail.less"
+
+const tabs = [
+ { tab: 'model.diagnose.tab.metrics', key: 'metrics', },
+ { tab: 'model.diagnose.tab.training', key: 'training', },
+]
+
+function ProjectDetail() {
+ const history = useHistory()
+ const location = useLocation()
+ const { id } = useParams()
+ const [active, setActive] = useState(tabs[0].key)
+ const [project, fetchProject] = useFetch('project/getProject')
+ const content = {
+ [tabs[0].key]:
,
+ [tabs[1].key]:
,
+ }
+
+ useEffect(() => {
+ id && fetchProject({ id, force: true })
+ }, [id])
+
+ useEffect(() => {
+ const tabKey = location.hash.replace(/^#/, '')
+ setActive(tabKey || tabs[0].key)
+ }, [location.hash])
+
+ function tabChange(key) {
+ history.push(`#${key}`)
+ }
+
+ return (
+
+
+ !tab.hidden).map(tab => ({ ...tab, tab: t(tab.tab) }))}
+ activeTabKey={active} onTabChange={tabChange} className='noShadow'
+ headStyle={{ background: '#fff', marginBottom: '10px' }}
+ bodyStyle={{ padding: '0 20px' }}>
+ {content[active]}
+
+
+ )
+}
+
+export default ProjectDetail
diff --git a/ymir/web/src/pages/project/diagnose/components/common.js b/ymir/web/src/pages/project/diagnose/components/common.js
new file mode 100644
index 0000000000..c602769895
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/components/common.js
@@ -0,0 +1,48 @@
+import { percent, toFixed } from '@/utils/number'
+import { Popover } from 'antd'
+import ReactJson from 'react-json-view'
+
+export function getModelCell(rid, tasks, models, text) {
+ const task = tasks.find(({ result }) => result === rid)
+ const model = models.find(model => model.id === task.model)
+ const stage = model.stages.find(sg => sg.id === task.stage)
+ const content =
+ const label = `${model.name} ${model.versionName} ${stage.name} ${task.configName}`
+ return text ? label :
+ {label}
+
+}
+
+
+export function getCK(data, keyword) {
+ const cks = Object.values(data).map(({ iou_averaged_evaluation }) => {
+ const ck = iou_averaged_evaluation.ck_evaluations[keyword] || {}
+ return ck.sub ? Object.keys(ck.sub) : []
+ }).flat()
+ const uniqueCKs = [...new Set(cks)]
+ return uniqueCKs.map(k => ({ value: k, label: k, parent: keyword }))
+}
+
+export const opt = d => ({ value: d.id, label: `${d.name} ${d.versionName}`, })
+
+export const average = (nums = []) => nums.reduce((prev, num) => !Number.isNaN(num) ? prev + num : prev, 0) / nums.length
+
+export const getKwField = (evaluation, type) => {
+ const ev = evaluation[!type ? 'dataset_evaluation' : 'sub_cks']
+ if (type) {
+ // ck
+ return Object.keys(ev).reduce((prev, curr) => {
+ const ap = ev[curr]?.iou_averaged_evaluation?.ci_averaged_evaluation || {}
+ return {
+ ...prev,
+ [curr]: ap,
+ }
+ }, {})
+ } else {
+ return Object.values(ev.iou_evaluations)[0]['ci_evaluations']
+ }
+}
+
+export const percentRender = value => typeof value === 'number' && !Number.isNaN(value) ? percent(value) : '-'
+
+export const confidenceRender = value => typeof value === 'number' && !Number.isNaN(value) ? toFixed(value, 3) : '-'
diff --git a/ymir/web/src/pages/project/diagnose/components/curveView.js b/ymir/web/src/pages/project/diagnose/components/curveView.js
new file mode 100644
index 0000000000..10de988e6b
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/components/curveView.js
@@ -0,0 +1,135 @@
+import { useEffect, useState } from "react"
+import { Col, Row, Table } from "antd"
+import PrCurve from "./prCurve"
+import Panel from "@/components/form/panel"
+import { getCK, getKwField, getModelCell, opt } from "./common"
+
+const CurveView = ({ tasks, datasets, models, data, xType, kw: { keywords } }) => {
+ const [list, setList] = useState([])
+ const [dd, setDD] = useState([])
+ const [kd, setKD] = useState([])
+ const [xasix, setXAsix] = useState([])
+ const [dData, setDData] = useState(null)
+ const [kData, setKData] = useState(null)
+ const [hiddens, setHiddens] = useState({})
+ const kwType = 0
+
+ useEffect(() => {
+ if (data && keywords) {
+ generateDData(data)
+ generateKData(data)
+ } else {
+ setDData(null)
+ setKData(null)
+ }
+ }, [data, keywords])
+
+ useEffect(() => {
+ setDD(datasets.map(opt))
+ }, [datasets])
+
+ useEffect(() => {
+ if (data && keywords) {
+ const kws = keywords.map(k => ({ value: k, label: k }))
+ setKD(kws)
+ }
+ }, [keywords, data])
+
+ useEffect(() => {
+ // list
+ if (dData && kData) {
+ generateList(!xType)
+ setXAsix(xType ? dd : kd)
+ } else {
+ setList([])
+ }
+ }, [xType, dd, kd, dData, kData])
+
+ function generateDData(data) {
+ const ddata = Object.keys(data).reduce((prev, rid) => {
+ const fiou = getKwField(data[rid], kwType)
+ return {
+ ...prev,
+ [rid]: fiou,
+ }
+ }, {})
+ setDData(ddata)
+ }
+
+ function generateKData(data) {
+ const kdata = {}
+ Object.keys(data).forEach(id => {
+ const fiou = getKwField(data[id], kwType)
+ Object.keys(fiou).forEach(key => {
+ kdata[key] = kdata[key] || {}
+ kdata[key][id] = fiou[key]
+ })
+ })
+ setKData(kdata)
+ }
+
+ function generateList(isDs) {
+ const titles = isDs ? dd : kd
+ const list = titles.map(({ value, label }) => ({
+ id: value, label,
+ rows: isDs ? generateDsRows(value) : generateKwRows(value),
+ }))
+ setList(list)
+ }
+
+ function generateDsRows(tid) {
+ const tts = tasks.filter(({ testing }) => testing === tid)
+
+ return kd.map(({ value }) => {
+ const kwRows = tts.map(({ result: rid }) => {
+ const ddata = dData[rid] || {}
+ const _model = getModelCell(rid, tasks, models, 'text')
+ const line = ddata[value]?.pr_curve || []
+ return {
+ id: rid,
+ name: _model,
+ line,
+ }
+ })
+ return {
+ id: value,
+ title: value,
+ lines: kwRows,
+ }
+ })
+ }
+
+ const generateKwRows = (kw) => {
+ const kdata = kData[kw]
+
+ return dd.map(({ value: tid, label }) => {
+ const tks = tasks.filter(({ testing }) => testing === tid)
+ const lines = tks.map(({ testing, result }) => {
+ const _model = getModelCell(result, tasks, models, 'text')
+ return {
+ id: testing,
+ name: _model,
+ line: kdata ? kdata[result]?.pr_curve : [],
+ }
+ })
+ return {
+ id: tid,
+ title: label,
+ lines,
+ }
+ })
+ }
+
+ return list.map(({ id, label, rows }) =>
+
setHiddens(old => ({ ...old, [id]: !value }))} bg={false}>
+
+ {rows.map(({ id, title, lines }, index) =>
+
+
+ )}
+
+
+
)
+}
+
+export default CurveView
diff --git a/ymir/web/src/pages/project/diagnose/components/defaultStages.js b/ymir/web/src/pages/project/diagnose/components/defaultStages.js
new file mode 100644
index 0000000000..7891b9b27e
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/components/defaultStages.js
@@ -0,0 +1,37 @@
+import { useEffect, useState } from "react"
+import { Form, Select } from "antd"
+import Panel from "@/components/form/panel"
+import useFetch from "@/hooks/useFetch"
+import t from "@/utils/t"
+const DefaultStages = ({ diagnosing, models = [] }) => {
+ const [result, setStage] = useFetch('model/setRecommendStage')
+ const [uniqueModels, setModels] = useState([])
+
+ useEffect(() => {
+ const unique = models.reduce((prev, model) => prev.some(md => md.id === model.id) || model.stages.length < 2 ? prev : [...prev, model], [])
+ setModels(unique)
+ }, [models])
+
+ useEffect(() => {
+ if (result) {
+ uniqueModels.forEach(model => model.id === result.id && (model.recommendStage = result.recommendStage))
+ }
+ }, [result])
+
+
+ const clickHandle = (model, stage) => {
+ setStage({ model, stage })
+ }
+
+ return diagnosing && uniqueModels.length ?
+
+ {uniqueModels.map(model =>
+
+ clickHandle(model.id, value)}>
+ {model.stages.map(stage => {stage.name} )}
+
+ )}
+ : null
+}
+
+export default DefaultStages
diff --git a/ymir/web/src/pages/project/diagnose/components/del.js b/ymir/web/src/pages/project/diagnose/components/del.js
new file mode 100644
index 0000000000..2325560ed0
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/components/del.js
@@ -0,0 +1,40 @@
+import t from "@/utils/t"
+import confirm from '@/components/common/dangerConfirm'
+import { connect } from "dva"
+import { forwardRef, useImperativeHandle } from "react"
+
+const Del = forwardRef(({ delVisualization, ok = () => {} }, ref) => {
+ useImperativeHandle(ref, () => {
+ return {
+ del,
+ }
+ })
+
+ function del(id) {
+ confirm({
+ content: t('visualization.del.confirm.content'),
+ onOk: async () => {
+ const result = await delVisualization(id)
+ if (result) {
+ ok(id)
+ }
+ },
+ okText: t('common.del'),
+ })
+ }
+
+ return null
+})
+
+const actions = (dispatch) => {
+ return {
+ delVisualization(id) {
+ return dispatch({
+ type: 'visualization/delVisualization',
+ payload: id,
+ })
+ }
+ }
+}
+
+export default connect(null, actions, null, { forwardRef: true })(Del)
\ No newline at end of file
diff --git a/ymir/web/src/pages/project/diagnose/components/mapView.js b/ymir/web/src/pages/project/diagnose/components/mapView.js
new file mode 100644
index 0000000000..abda6ecfe5
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/components/mapView.js
@@ -0,0 +1,177 @@
+import { useEffect, useState } from "react"
+import { Col, Row, Table } from "antd"
+import { percent } from '@/utils/number'
+import { isSame } from '@/utils/object'
+import Panel from "@/components/form/panel"
+import { average, getCK, getKwField, getModelCell, opt, percentRender } from "./common"
+
+const MapView = ({ tasks, datasets, models, data, xType, kw: { kwType, keywords } }) => {
+ const [list, setList] = useState([])
+ const [dd, setDD] = useState([])
+ const [kd, setKD] = useState([])
+ const [xasix, setXAsix] = useState([])
+ const [dData, setDData] = useState(null)
+ const [kData, setKData] = useState(null)
+ const [columns, setColumns] = useState([])
+ const [hiddens, setHiddens] = useState({})
+
+ useEffect(() => {
+ if (data && keywords) {
+ generateDData(data)
+ generateKData(data)
+ } else {
+ setDData(null)
+ setKData(null)
+ }
+ }, [kwType, data, keywords])
+
+ useEffect(() => {
+ setDD(datasets.map(opt))
+ }, [datasets])
+
+ useEffect(() => {
+ if (data && keywords) {
+ const kws = keywords.map(k => ({ value: k, label: k }))
+ setKD(kws)
+ }
+ }, [keywords, data, kwType])
+
+ useEffect(() => {
+ const cls = generateColumns()
+ setColumns(cls)
+ }, [xasix])
+
+ useEffect(() => {
+ // list
+ if (dData && kData) {
+ generateList(!xType)
+ setXAsix(xType ? dd : kd)
+ } else {
+ setList([])
+ }
+ }, [xType, dd, kd, dData, kData])
+
+ function generateDData(data) {
+ const ddata = Object.keys(data).reduce((prev, rid) => {
+ const fiou = getKwField(data[rid], kwType)
+ return {
+ ...prev,
+ [rid]: fiou,
+ }
+ }, {})
+ setDData(ddata)
+ }
+
+ function generateKData(data) {
+ const kdata = {}
+ Object.keys(data).forEach(id => {
+ const fiou = getKwField(data[id], kwType)
+ Object.keys(fiou).forEach(key => {
+ kdata[key] = kdata[key] || {}
+ kdata[key][id] = fiou[key]
+ })
+ })
+ setKData(kdata)
+ }
+
+ function generateList(isDs) {
+ const titles = isDs ? dd : kd
+ const list = titles.map(({ value, label }) => ({
+ id: value, label,
+ rows: isDs ? generateDsRows(value) : generateKwRows(value),
+ }))
+ setList(list)
+ }
+
+ function generateDsRows(tid) {
+ const tts = tasks.filter(({ testing }) => testing === tid)
+ return tts.map(({ result: rid }) => {
+ const ddata = dData[rid] || {}
+ const kwAps = kd.reduce((prev, { value: kw }) => {
+ return {
+ ...prev,
+ [kw]: ddata[kw]?.ap,
+ }
+ }, {})
+ const _average = average(Object.values(kwAps))
+ const _model = getModelCell(rid, tasks, models)
+ return {
+ id: rid,
+ _model,
+ _average,
+ ...kwAps,
+ }
+ })
+ }
+
+ const generateKwRows = (kw) => {
+ const kdata = kData[kw]
+
+ const mids = Object.values(tasks.reduce((prev, { model, stage, config }) => {
+ const id = `${model}${stage}${JSON.stringify(config)}`
+ return {
+ ...prev,
+ [id]: { id, mid: model, sid: stage, config: config },
+ }
+ }, {}))
+
+ return mids.map(({ id, mid, sid, config }) => {
+ const tts = tasks.filter(({ model, stage, config: tconfig }) => model === mid && stage === sid && isSame(config, tconfig))
+ const _model = getModelCell(tts[0].result, tasks, models)
+
+ const drow = kdata ? tts.reduce((prev, { testing, result }) => {
+ return {
+ ...prev,
+ [testing]: kdata[result]?.ap,
+ }
+ }, {}) : {}
+ const _average = average(Object.values(drow))
+ return {
+ id,
+ config,
+ _model,
+ _average,
+ ...drow,
+ }
+ }).flat()
+ }
+
+ function generateColumns() {
+ const dynamicColumns = xasix.map(({ value, label }) => ({
+ title: label,
+ dataIndex: value,
+ width: 100,
+ render: percentRender,
+ }))
+ return [
+ {
+ title: 'Model',
+ dataIndex: '_model',
+ width: 150,
+ ellipsis: true,
+ },
+ {
+ title: 'Average mAP',
+ dataIndex: '_average',
+ width: 100,
+ render: percentRender,
+ },
+ ...dynamicColumns,
+ ]
+ }
+
+ return list.map(({ id, label, rows }) =>
+
setHiddens(old => ({ ...old, [id]: !value }))} bg={false}>
+ record.id}
+ rowClassName={(record, index) => index % 2 === 0 ? '' : 'oddRow'}
+ columns={columns}
+ pagination={false}
+ scroll={{ x: '100%' }}
+ />
+
+ )
+}
+
+export default MapView
diff --git a/ymir/web/src/pages/project/diagnose/components/prCurve.js b/ymir/web/src/pages/project/diagnose/components/prCurve.js
new file mode 100644
index 0000000000..b21a211c6e
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/components/prCurve.js
@@ -0,0 +1,96 @@
+import LineChart from "@/components/chart/line"
+import { useEffect, useState } from "react"
+import { Card } from "antd"
+
+import Empty from '@/components/empty/default'
+
+
+const PrCurve = ({ title='', lines }) => {
+ const [option, setOption] = useState({})
+ const [series, setSeries] = useState([])
+ const [xasix, setXAsix] = useState([])
+ const tooltip = {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ }
+
+ const legend = { }
+ const grid = {
+ left: 20,
+ right: 20,
+ containLabel: true
+ }
+ const yAxis = [
+ {
+ type: 'value',
+ name: 'Precision',
+ nameLocation: 'center',
+ nameGap: 30,
+ }
+ ]
+
+ useEffect(() => {
+ transferLines(lines)
+ }, [lines])
+
+ useEffect(() => {
+ if (!series.length) {
+ return
+ }
+ const xAxis = [
+ {
+ type: 'category',
+ name: 'Recall',
+ nameLocation: 'center',
+ nameGap: 30,
+ data: xasix,
+ }
+ ]
+ setOption({
+ tooltip,
+ legend,
+ grid,
+ yAxis,
+ xAxis,
+ series,
+ })
+ }, [xasix, series])
+
+ function getP(line, field = 'x') {
+ return line ? line.map(point => point[field]) : null
+ }
+
+ function transferLines(lines = []) {
+ let xdata = lines.map(({ line }) => getP(line)).flat()
+ const series = lines.map(({ name, line }) => {
+ const ydata = getP(line, 'y')
+ return {
+ name,
+ type: 'line',
+ smooth: true,
+ showSymbol: false,
+ label: { show: true, formatter: '{@[0]}' },
+ emphasis: {
+ focus: 'series'
+ },
+ data: ydata,
+ }
+ })
+
+ setXAsix([...new Set(xdata)])
+ setSeries(series)
+ }
+
+ return (
+
+ {series.length ? (
+ <>
+
+ >) : }
+
+ )
+}
+
+export default PrCurve
diff --git a/ymir/web/src/pages/project/diagnose/components/prView.js b/ymir/web/src/pages/project/diagnose/components/prView.js
new file mode 100644
index 0000000000..7fdb99015c
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/components/prView.js
@@ -0,0 +1,269 @@
+import { useEffect, useState } from "react"
+import { Table } from "antd"
+import { percent, toFixed } from '@/utils/number'
+import { isSame } from '@/utils/object'
+import t from '@/utils/t'
+import Panel from "@/components/form/panel"
+import { average, getCK, getKwField, opt, percentRender, getModelCell, confidenceRender } from "./common"
+
+const getLabels = type => ({
+ colMain: `model.diagnose.metrics.${type}.label`,
+ colAverage: `model.diagnose.metrics.${type}.average.label`,
+ colTarget: `model.diagnose.metrics.${type}.target.label`,
+})
+
+
+function generateRange(min, max, step = 0.05) {
+ let result = []
+ let current = min * 100
+ while (current <= max * 100) {
+ result.push(current / 100)
+ current += step * 100
+ }
+ return result
+}
+
+function rangePoints(range, points = [], field = 'x') {
+ return range.map(value => {
+ return points?.reduce((prev, curr) =>
+ Math.abs(prev[field] - value) <= Math.abs(curr[field] - value) ? prev : curr, 1)
+ })
+}
+function findClosestPoint(target, points = [], field = 'x') {
+ return points?.reduce((prev, curr) => Math.abs(prev[field] - target) <= Math.abs(curr[field] - target) ? prev : curr, 1)
+}
+
+const PView = ({ tasks, datasets, models, data, prType, prRate, xType, kw: { keywords } }) => {
+ const [list, setList] = useState([])
+ const [dd, setDD] = useState([])
+ const [kd, setKD] = useState([])
+ const [xasix, setXAsix] = useState([])
+ const [dData, setDData] = useState(null)
+ const [kData, setKData] = useState(null)
+ const [columns, setColumns] = useState([])
+ const [range, setRange] = useState([])
+ const [pointField, setPointField] = useState(['x', 'y'])
+ const [labels, setLabels] = useState({})
+ const [hiddens, setHiddens] = useState({})
+ const kwType = 0
+
+ useEffect(() => {
+ const min = prRate[0]
+ const max = prRate[1]
+ setRange(generateRange(min, max))
+ }, [prRate])
+
+ useEffect(() => {
+ setPointField(prType ? ['y', 'x'] : ['x', 'y'])
+ setLabels(getLabels(prType ? 'recall' : 'precision'))
+ }, [prType])
+
+ useEffect(() => {
+ if (data && keywords) {
+ generateDData(data)
+ generateKData(data)
+ } else {
+ setDData(null)
+ setKData(null)
+ }
+ }, [data, keywords])
+
+ useEffect(() => {
+ setDD(datasets.map(opt))
+ }, [datasets])
+
+ useEffect(() => {
+ if (data && keywords) {
+ const kws = keywords.map(k => ({ value: k, label: k }))
+ setKD(kws)
+ }
+ }, [keywords, data])
+
+ useEffect(() => {
+ const cls = generateColumns()
+ setColumns(cls)
+ }, [xasix])
+
+ useEffect(() => {
+ // list
+ if (dData && kData) {
+ generateList(!xType)
+ setXAsix(xType ? dd : kd)
+ } else {
+ setList([])
+ }
+ }, [xType, dd, kd, dData, kData, range])
+
+ function generateDData(data) {
+ const ddata = Object.keys(data).reduce((prev, rid) => {
+ const fiou = getKwField(data[rid], kwType)
+ return {
+ ...prev,
+ [rid]: fiou,
+ }
+ }, {})
+ setDData(ddata)
+ }
+
+ function generateKData(data) {
+ const kdata = {}
+ Object.keys(data).forEach(id => {
+ const fiou = getKwField(data[id], kwType)
+ Object.keys(fiou).forEach(key => {
+ kdata[key] = kdata[key] || {}
+ kdata[key][id] = fiou[key]
+
+ })
+ })
+ setKData(kdata)
+ }
+
+ function generateList(isDs) {
+ const titles = isDs ? dd : kd
+ const list = titles.map(({ value, label }) => ({
+ id: value, label,
+ rows: isDs ? generateDsRows(value) : generateKwRows(value),
+ }))
+ setList(list)
+ }
+
+ function generateDsRows(tid) {
+ const tts = tasks.filter(({ testing }) => testing === tid)
+
+ return tts.map(({ model, result: rid }) => {
+ return range.map(rate => {
+ const ddata = dData[rid] || {}
+
+ const _model = getModelCell(rid, tasks, models)
+ const kwPoints = kd.map(({ value: kw }) => {
+ const line = ddata[kw]?.pr_curve
+ return { point: findClosestPoint(rate, line, pointField[0]), kw }
+ })
+ const _average = average(kwPoints.map(({ point }) => point[pointField[1]]))
+ const confidenceAverage = average(kwPoints.map(({ point: { z } }) => z))
+ const kwFields = kwPoints.reduce((prev, { kw, point }) => ({
+ ...prev,
+ [`${kw}_target`]: point[pointField[1]],
+ [`${kw}_conf`]: point.z,
+ }), {})
+
+ return {
+ id: `${rid}${rate}`,
+ value: rate,
+ name: _model,
+ ...kwFields,
+ a: _average,
+ ca: confidenceAverage,
+ }
+ })
+ }).flat()
+ }
+
+ // todo
+ function generateKwRows(kw) {
+ const kdata = kData[kw] || {}
+
+
+ const mids = Object.values(tasks.reduce((prev, { model, stage, config }) => {
+ const id = `${model}${stage}${JSON.stringify(config)}`
+ return {
+ ...prev,
+ [id]: { id, mid: model, sid: stage, config: config },
+ }
+ }, {}))
+
+ return mids.map(({ id, mid, sid, config }) => {
+ const tts = tasks.filter(({ model, stage, config: tconfig }) => model === mid && stage === sid && isSame(config, tconfig))
+ const _model = getModelCell(tts[0].result, tasks, models)
+
+ return range.map(rate => {
+ const points = tts.map(({ result: rid, testing: tid }) => {
+ const line = kdata[rid]?.pr_curve
+ const point = findClosestPoint(rate, line, pointField[0])
+ return {
+ tid,
+ point,
+ }
+ })
+ const _average = average(points.map(({point}) => point[pointField[1]]))
+ const confidenceAverage = average(points.map(({ point: { z }}) => z))
+ const tpoints = points.reduce((prev, { tid, point }) => ({
+ ...prev,
+ [`${tid}_target`]: point[pointField[1]],
+ [`${tid}_conf`]: point.z,
+ }), {})
+
+ return {
+
+ id: `${id}${rate}`,
+ value: rate,
+ name: _model,
+ ...tpoints,
+ a: _average,
+ ca: confidenceAverage,
+ }
+ })
+ }).flat()
+ }
+
+ function generateColumns() {
+ const dynamicColumns = xasix.map(({ value, label }) => ([
+ {
+ title: t(labels.colTarget, { label: {label}
}),
+ dataIndex: `${value}_target`,
+ colSpan: 2,
+ width: 100,
+ render: percentRender,
+ }, {
+ colSpan: 0,
+ dataIndex: `${value}_conf`,
+ width: 100,
+ render: confidenceRender,
+ },
+ ])).flat()
+ return [
+ {
+ title: 'Model',
+ dataIndex: 'name',
+ width: 150,
+ onCell: (_, index) => ({
+ rowSpan: index % range.length ? 0 : range.length,
+ }),
+ },
+ {
+ title: t(labels.colMain),
+ dataIndex: 'value',
+ width: 100,
+ render: percentRender,
+ },
+ ...dynamicColumns,
+ {
+ title: t(labels.colAverage),
+ dataIndex: 'a',
+ width: 100,
+ render: percentRender,
+ },
+ {
+ title: t('model.diagnose.metrics.confidence.average.label'),
+ dataIndex: 'ca',
+ width: 100,
+ render: confidenceRender,
+ },
+ ]
+ }
+
+ return list.map(({ id, label, rows }) =>
+
setHiddens(old => ({ ...old, [id]: !value }))} bg={false}>
+ record.id}
+ rowClassName={(record, index) => Math.floor(index / range.length) % 2 === 0 ? '' : 'oddRow'}
+ columns={columns}
+ pagination={false}
+ scroll={{ x: '100%' }}
+ />
+
+ )
+}
+
+export default PView
diff --git a/ymir/web/src/pages/project/diagnose/components/view.js b/ymir/web/src/pages/project/diagnose/components/view.js
new file mode 100644
index 0000000000..4e608e54d8
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/components/view.js
@@ -0,0 +1,8 @@
+const View = (Component) => {
+ const Viewer = (props) => {
+ return
+ }
+ return Viewer
+}
+
+export default View
diff --git a/ymir/web/src/pages/project/diagnose/index.less b/ymir/web/src/pages/project/diagnose/index.less
new file mode 100644
index 0000000000..4dfd03b237
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/index.less
@@ -0,0 +1,73 @@
+.wrapper {
+ height: 100%;
+}
+.container {
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 180px);
+ :global(.ant-card-body) {
+ flex: 1;
+ overflow-y: auto;
+ }
+}
+.formContainer {
+ position: relative;
+}
+.mask {
+ position: absolute;
+ width: calc(100% + 20px);
+ height: calc(100% + 10px);
+ display: flex;
+ align-items: end;
+ justify-content: center;
+ background-color: rgba(0, 0, 0, 0.1);
+ z-index: 5;
+ margin: 0 -10px -10px;
+}
+.trainingMask {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ // background-color: rgba(0, 0, 0, 0.1);
+ background-color: #fff;
+ z-index: 5;
+ .nameList {
+ flex: 1;
+ }
+}
+
+.extra {
+ padding: 2px 8px;
+ font-size: 12px;
+ color:rgba(232, 185, 0, 1);
+ background-color: rgba(250, 211, 55, 0.1);
+}
+:global(body .ant-tooltip-placement-top) {
+ z-index: 1000;
+}
+.filterPanel {
+ padding: 10px 20px;
+ background: rgba(44, 189, 233, 0.1);
+}
+.prRate {
+ width: 200px;
+ &:hover {
+
+ :global(.ant-slider-rail) {
+ background-color: lighten(@primary-color, 10) !important;
+ }
+ :global(.ant-slider-track) {
+ background-color: darken(@primary-color, 5) !important;
+ }
+ }
+ :global(.ant-slider-rail) {
+ background-color: lighten(@primary-color, 30);
+ }
+ :global(.ant-slider-track) {
+ background-color: darken(@primary-color, 10);
+ }
+}
diff --git a/ymir/web/src/pages/project/diagnose/metrics.js b/ymir/web/src/pages/project/diagnose/metrics.js
new file mode 100644
index 0000000000..80bc1144f4
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/metrics.js
@@ -0,0 +1,288 @@
+import React, { useCallback, useEffect, useState } from "react"
+import { Card, Button, Form, Row, Col, Radio, Slider, Select, InputNumber, Checkbox, Space, Tag, } from "antd"
+
+import t from "@/utils/t"
+import useFetch from "@/hooks/useFetch"
+import Panel from "@/components/form/panel"
+import InferResultSelect from "@/components/form/inferResultSelect"
+import MapView from "./components/mapView"
+import CurveView from "./components/curveView"
+import PView from "./components/prView"
+import View from './components/view'
+import DefaultStages from "./components/defaultStages"
+
+import s from "./index.less"
+import { CompareIcon } from "@/components/common/icons"
+
+const metricsTabs = [
+ { value: 'map', component: MapView, ck: true },
+ { value: 'curve', component: CurveView, },
+ { value: 'rp', component: PView, },
+ { value: 'pr', component: PView, },
+]
+
+const xAxisOptions = [
+ { key: 'dataset', value: 0 },
+ { key: 'keyword', value: 1 },
+]
+
+const kwTypes = [{ label: 'keyword.add.name.label', value: 0 }, { label: 'keyword.ck.label', value: 1 }]
+
+function Matrics({ pid, project }) {
+ const [form] = Form.useForm()
+ const [inferTasks, setInferTasks] = useState([])
+ const [selectedModels, setSelectedModels] = useState([])
+ const [selectedDatasets, setSelectedDatasets] = useState([])
+ const [iou, setIou] = useState(0.5)
+ const [everageIou, setEverageIou] = useState(false)
+ const [confidence, setConfidence] = useState(0.3)
+ const [selectedMetric, setSelectedMetric] = useState(metricsTabs[0].value)
+ const [prRate, setPrRate] = useState([0.8, 0.95])
+ const [keywords, setKeywords] = useState([])
+ const [selectedKeywords, setSelectedKeywords] = useState([])
+ const [subCks, setSubCks] = useState([])
+ const [kwType, setKwType] = useState(0)
+ const [kws, setKws] = useState([])
+ const [xAxis, setXAsix] = useState(xAxisOptions[0].value)
+ const [remoteData, fetchDiagnosis] = useFetch('dataset/evaluate')
+ const [diagnosis, setDiagnosis] = useState(null)
+ const [diagnosing, setDiagnosing] = useState(false)
+ const [kwFilter, setKwFilter] = useState({
+ kwType: 0,
+ keywords: [],
+ })
+ const [ckDatasets, getCKDatasets] = useFetch('dataset/getCK', [])
+ const [cks, setCKs] = useState([])
+ const selectedCK = Form.useWatch('ck', form)
+
+ useEffect(() => {
+ setDiagnosis(remoteData)
+ }, [remoteData])
+
+ useEffect(() => {
+ if (diagnosing) {
+ const kws = [...new Set(selectedModels.map(({ keywords }) => keywords).flat())]
+ setKeywords(kws)
+ } else {
+ setKeywords([])
+ }
+ }, [selectedModels, diagnosing])
+
+ useEffect(() => {
+ // calculate ck
+ const cks = diagnosis ?
+ Object.values(diagnosis).map(({ sub_cks }) => Object.keys(sub_cks)).flat() : []
+
+ setSubCks([...new Set(cks)])
+ }, [diagnosis])
+
+ useEffect(() => {
+ setKws(!kwType ? keywords : subCks)
+ }, [kwType, keywords, subCks])
+
+ useEffect(() => {
+ setDiagnosing(!!diagnosis)
+ setSelectedKeywords([])
+ }, [diagnosis])
+
+ useEffect(() => {
+ setKwFilter({
+ keywords: selectedKeywords?.length ? selectedKeywords : kws,
+ kwType,
+ })
+ }, [selectedKeywords, kws])
+
+ useEffect(() => {
+ if (selectedDatasets.length) {
+ getCKDatasets({ pid, ids: selectedDatasets.map(({ id }) => id) })
+ }
+ }, [selectedDatasets])
+
+ useEffect(() => {
+ const allCks = ckDatasets.map(({ cks: { keywords } }) => keywords.map(({ keyword }) => keyword)).flat()
+ const cks = allCks.filter(keyword => {
+ const same = allCks.filter(k => k === keyword)
+ return same.length === ckDatasets.length
+ })
+ const uniqueCks = [...new Set(cks)]
+ setCKs(uniqueCks)
+ }, [ckDatasets])
+
+ const onFinish = async (values) => {
+ const inferDataset = inferTasks.map(({ result }) => result)
+ const params = {
+ ...values,
+ projectId: pid,
+ everageIou,
+ datasets: inferDataset,
+ }
+ fetchDiagnosis(params)
+ }
+
+ function onFinishFailed(errorInfo) {
+ console.log("Failed:", errorInfo)
+ }
+
+ function inferResultChange({ tasks, models, datasets }) {
+ setInferTasks(tasks.map(
+ ({ config, configName, parameters: { dataset_id, model_id, model_stage_id }, result_dataset: { id } }) =>
+ ({ config, configName, testing: dataset_id, model: model_id, stage: model_stage_id, result: id })))
+ setSelectedDatasets(datasets)
+ setSelectedModels(models)
+ form.setFieldsValue({
+ ck: undefined
+ })
+ }
+
+ function metricsChange({ target: { value } }) {
+ setSelectedMetric(value)
+ const tab = metricsTabs.find(t => t.value === value)
+ if (!tab.ck) {
+ setKwType(0)
+ }
+ }
+
+ function prRateChange(value) {
+ setPrRate(value)
+ }
+
+ function xAxisChange({ target: { value } }) {
+ setXAsix(value)
+ }
+
+ function kwChange(values) {
+ setSelectedKeywords(values)
+ }
+
+ function retry() {
+ setDiagnosis(null)
+ setDiagnosing(false)
+ setKwType(0)
+ }
+
+ function renderView() {
+ const panel = metricsTabs.find(({ value }) => selectedMetric === value)
+ const Viewer = View(panel.component)
+ return
+ }
+
+ const renderFilterPanel = () =>
+
+ {t('model.diagnose.metrics.view.label')}
+ ({ ...item, label: t(`model.diagnose.medtric.tabs.${item.value}`) }))}
+ onChange={metricsChange}
+ />
+
+
+
+
+
+
+ {
+ const tab = metricsTabs.find(({ value }) => selectedMetric === value)
+ return tab.ck || !type.value
+ }).map(({ label, value }) => ({ value, label: t(label) }))} onChange={setKwType}>
+
+
+ {kwTypes[0].value === kwType ? ({ label: kw, value: kw }))}
+ placeholder={t(kwType ? 'model.diagnose.metrics.ck.placeholder' : 'model.diagnose.metrics.keyword.placeholder')}
+ showArrow onChange={kwChange}> :
+ {selectedCK} }
+
+
+
+ {t('model.diagnose.metrics.dimension.label')}
+ ({ value, label: t(`model.diagnose.metrics.x.${key}`) }))} onChange={xAxisChange} />
+
+
+
+
+
+ const renderViewPanel = () => {renderView()}
+
+ const renderIouTitle =
+ {t('model.diagnose.form.iou')}
+ setEverageIou(checked)}>Average IOU
+
+
+ const iouOptions = [
+ { value: true, label: t('model.diagnose.form.iou.everage') },
+ { value: false, label: t('model.diagnose.form.iou.single') },
+ ]
+
+ // todo form initial values
+ const initialValues = {
+ iou,
+ confidence,
+ }
+ return (
+
+
+
+ {renderFilterPanel()}
+ {renderViewPanel()}
+
+
+
+
+ retry()}> {t('model.diagnose.metrics.btn.retry')}
+
+
+
+
+
+
+
+
+ ({ value: ck, label: ck }))} allowClear>
+
+
+ setEverageIou(value)} options={iouOptions}>
+
+
+
+
+
+
+
+ {t('model.diagnose.metrics.btn.start')}
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Matrics
diff --git a/ymir/web/src/pages/project/diagnose/training.js b/ymir/web/src/pages/project/diagnose/training.js
new file mode 100644
index 0000000000..bc0205f707
--- /dev/null
+++ b/ymir/web/src/pages/project/diagnose/training.js
@@ -0,0 +1,126 @@
+import React, { useEffect, useRef, useState } from "react"
+import { Card, Button, Form, Row, Col, Table, } from "antd"
+
+import t from "@/utils/t"
+import Panel from "@/components/form/panel"
+import ModelSelect from "@/components/form/modelSelect"
+
+import s from "./index.less"
+import { CompareIcon } from "@/components/common/icons"
+import { getTensorboardLink } from "../../../services/common"
+
+function Training({ pid, project }) {
+ const [form] = Form.useForm()
+ const iframe = useRef(null)
+ const [selectedModels, setSelectedModels] = useState([])
+ const [tensorboardUrl, setUrl] = useState('')
+ const [hasResult, setHasResult] = useState(false)
+ const [modelMap, setModelMap] = useState([])
+ const nameColumns = [
+ {title: 'model', dataIndex: 'name', ellipsis: true, width: '50%' },
+ {title: 'hash', dataIndex: 'hash', ellipsis: true },
+ ]
+
+ useEffect(() => {
+ const maps = selectedModels.map(model => ({
+ id: model.id,
+ name: `${model.name} ${model.versionName}`,
+ hash: model.task.hash,
+ }))
+ setModelMap(maps)
+ }, [hasResult, selectedModels])
+
+ useEffect(() => {
+ if (iframe.current && tensorboardUrl) {
+ iframe.current.onload = () => {
+ hideSidebar()
+ }
+ }
+ }, [iframe.current, tensorboardUrl])
+
+ const onFinish = async () => {
+ let url = ''
+ if (selectedModels.length) {
+ const hashs = selectedModels.map(model => model.task.hash)
+ url = getTensorboardLink(hashs)
+ }
+ setUrl(url)
+ setHasResult(true)
+ }
+
+ function onFinishFailed(errorInfo) {
+ console.log("Failed:", errorInfo)
+ }
+
+ function hideSidebar() {
+ setTimeout(() => {
+ try {
+ const document = iframe.current.contentWindow.document
+ const sidebarShadowRoot = document.getElementsByTagName('tf-scalar-dashboard')[0].shadowRoot.children[1].shadowRoot
+
+ var style = document.createElement('style')
+ style.innerHTML = '#sidebar { display: none }'
+ sidebarShadowRoot.appendChild(style)
+ } catch (e) {
+ hideSidebar()
+ }
+ }, 100)
+ }
+
+ function modelChange(id, options = []) {
+ setSelectedModels(options.map(([{ model }]) => model) || [])
+ }
+
+ function retry() {
+ setHasResult(false)
+ setUrl('')
+ }
+
+ const initialValues = {}
+ return (
+
+
+
+ {tensorboardUrl ?
+ : ''}
+
+
+
+
+
r.id} />
+
+ retry()}>
+ {t('model.action.diagnose.training.retry')}
+
+
+
+
+
+
+
+
+
+
+ {t('common.action.diagnose.training')}
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Training
diff --git a/ymir/web/src/pages/project/hidden.js b/ymir/web/src/pages/project/hidden.js
index ed1fb594d4..4119c73130 100644
--- a/ymir/web/src/pages/project/hidden.js
+++ b/ymir/web/src/pages/project/hidden.js
@@ -1,7 +1,6 @@
import React, { useEffect, useState } from "react"
import s from "./index.less"
import { useHistory, useLocation, useParams } from "umi"
-import { Card, } from "antd"
import t from "@/utils/t"
import { tabs } from '@/constants/project'
@@ -9,7 +8,6 @@ import Breadcrumbs from "@/components/common/breadcrumb"
import HiddenList from "./components/hiddenList"
function Hidden() {
- const history = useHistory()
const location = useLocation()
const { id } = useParams()
const [active, setActive] = useState(tabs[0].key)
@@ -18,22 +16,11 @@ function Hidden() {
const tabKey = location.hash.replace(/^#/, '')
setActive(tabKey || tabs[0].key)
}, [location.hash])
-
- function tabChange(key) {
- history.push(`#${key}`)
- }
return (
- ({ ...tab, tab: t(tab.tab) }))} activeTabKey={active} onTabChange={tabChange}
- className='noShadow'
- bordered={false}
- style={{ margin: '-20px -5vw 0', background: 'transparent' }}
- headStyle={{ padding: '0 5vw', background: '#fff', marginBottom: '20px' }}
- bodyStyle={{ padding: '0 5vw' }}>
-
-
+
)
}
diff --git a/ymir/web/src/pages/project/iterationSettings.js b/ymir/web/src/pages/project/iterationSettings.js
new file mode 100644
index 0000000000..922a4e0d18
--- /dev/null
+++ b/ymir/web/src/pages/project/iterationSettings.js
@@ -0,0 +1,159 @@
+import { useCallback, useEffect, useState } from 'react'
+import { Button, Card, Form, message, Modal, Select, Space, Row, Col, InputNumber } from 'antd'
+import { useParams, useHistory } from "umi"
+
+import s from './add.less'
+import t from '@/utils/t'
+import { MiningStrategy } from '@/constants/iteration'
+import Breadcrumbs from '@/components/common/breadcrumb'
+import DatasetSelect from '../../components/form/datasetSelect'
+import useFetch from '../../hooks/useFetch'
+
+const { useForm } = Form
+const { confirm } = Modal
+
+const strategyOptions = Object.values(MiningStrategy)
+ .filter(key => Number.isInteger(key))
+ .map(value => ({
+ value,
+ label: t(`project.mining.strategy.${value}`),
+ }))
+
+const Add = ({ }) => {
+ const { id } = useParams()
+ const history = useHistory()
+ const [form] = useForm()
+ const [project, getProject] = useFetch('project/getProject', {})
+ const [result, updateIteration] = useFetch('project/updateProject')
+
+ const [testSet, setTestSet] = useState(0)
+ const [miningSet, setMiningSet] = useState(0)
+ const [strategy, setStrategy] = useState(0)
+
+ useEffect(() => {
+ id && getProject({ id })
+ }, [id])
+
+ useEffect(() => {
+ setTimeout(() => initForm(project), 1000)
+ }, [project])
+
+ useEffect(() => {
+ if (result) {
+ message.success(t(`project.update.success`))
+ history.goBack()
+ }
+ }, [result])
+
+ function initForm(project = {}) {
+ const { name, trainSetVersion, testSet: testDataset, miningSet: miningDataset, miningStrategy, chunkSize } = project
+ if (name) {
+ form.setFieldsValue({
+ trainSetVersion,
+ testSet: testDataset?.id || undefined,
+ miningSet: miningDataset?.id || undefined,
+ strategy: miningStrategy || 0,
+ chunkSize: miningStrategy === 0 && chunkSize ? chunkSize : undefined,
+ })
+ setStrategy(miningStrategy)
+ }
+ }
+
+ const submit = async ({ name = '', description = '', ...values }) => {
+ var params = {
+ ...values,
+ id,
+ chunkSize: strategy === 0 ? values.chunkSize : undefined,
+ }
+
+ updateIteration(params)
+ }
+
+ const miningFilter = useCallback(datasets => datasets.filter(ds => ds.keywords.some(kw => project?.keywords?.includes(kw))), [project?.keywords])
+
+ return (
+
+
+
+
+
+
+
+ {project.trainSet?.name}
+
+
+
+ {project?.trainSet?.versions?.map(({ id, versionName, assetCount }) =>
+ {versionName} (assets: {assetCount})
+ )}
+
+
+
+
+
+
+ setTestSet(value)}
+ allowClear
+ />
+
+
+ setMiningSet(value)}
+ allowClear
+ />
+
+
+
+
+
+ setStrategy(value)} />
+
+
+ {strategy === MiningStrategy.block ?
+
+
+
+ : null}
+
+
+
+
+
+
+ {t('common.confirm')}
+
+
+
+ history.goBack()}>
+ {t('common.back')}
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Add
diff --git a/ymir/web/src/pages/project/iterations.js b/ymir/web/src/pages/project/iterations.js
index 74c09883af..f94d0fccc9 100644
--- a/ymir/web/src/pages/project/iterations.js
+++ b/ymir/web/src/pages/project/iterations.js
@@ -1,200 +1,53 @@
-import React, { useEffect, useState } from "react"
-import { connect } from 'dva'
-import s from "./index.less"
-import { useHistory, useParams } from "umi"
-import { Form, Table, Modal, ConfigProvider, Card, Space, Row, Col, Button, Popover, } from "antd"
+import React, { useCallback, useEffect, useState } from "react"
+import { useParams } from "umi"
import t from "@/utils/t"
-import { percent, isNumber } from '@/utils/number'
-import { getStageLabel } from '@/constants/project'
+import useFetch from '@/hooks/useFetch'
import Breadcrumbs from "@/components/common/breadcrumb"
-import KeywordRates from "@/components/dataset/keywordRates"
+import Iteration from './iterations/iteration'
+import Prepare from "./iterations/prepare"
+import Current from './iterations/detail'
+import List from "./iterations/list"
-function Iterations({ ...func }) {
- const history = useHistory()
- const { id } = useParams()
- const [project, setProject] = useState({})
- const [iterations, setIterations] = useState([])
+import s from "./iterations/index.less"
+import { CardTabs } from "@/components/tabs/cardTabs"
+import ProjectDetail from "./components/detail"
- useEffect(() => {
- if (id) {
- fetchIterations()
- fetchProject()
- }
- }, [id])
+function Iterations() {
+ const { id } = useParams()
+ const [iterations, getIterations] = useFetch('iteration/getIterations', [])
+ const [project, getProject, setProject] = useFetch('project/getProject', {})
- const columns = [
- {
- title: showTitle("iteration.column.round"),
- dataIndex: "round",
- render: (round) => (t('iteration.round.label', { round })),
- },
- {
- title: showTitle("iteration.column.premining"),
- dataIndex: "miningDatasetLabel",
- render: (label, { versionName, miningDataset }) => renderPop(label, miningDataset),
- ellipsis: true,
- },
- {
- title: showTitle("iteration.column.mining"),
- dataIndex: "miningResultDatasetLabel",
- render: (label, { miningResultDataset }) => renderPop(label, miningResultDataset),
- ellipsis: true,
- },
- {
- title: showTitle("iteration.column.label"),
- dataIndex: "labelDatasetLabel",
- render: (label, { labelDataset }) => renderPop(label, labelDataset),
- align: 'center',
- ellipsis: true,
- },
- {
- title: showTitle("iteration.column.test"),
- dataIndex: "testDatasetLabel",
- render: (label, { testDataset }) => renderPop(label, testDataset),
- align: 'center',
- ellipsis: true,
- },
- {
- title: showTitle("iteration.column.merging"),
- dataIndex: "trainUpdateDatasetLabel",
- render: (label, { trainEffect, trainUpdateDataset }) => renderPop(label, trainUpdateDataset,
- {renderExtra(trainEffect)} ),
- align: 'center',
- ellipsis: true,
- },
- {
- title: showTitle("iteration.column.training"),
- dataIndex: 'map',
- render: (map, { mapEffect }) =>
- {map >= 0 ? percent(map) : null}
- {renderExtra(mapEffect, true)}
-
,
- align: 'center',
- },
+ const tabs = [
+ { tab: t('project.iteration.tabs.current'), key: 'current', content: },
+ { tab: t('project.iteration.tabs.list'), key: 'list', content:
},
]
- function renderPop(label, dataset = {}, extra) {
- dataset.project = project
- const content =
- return
- {label}
- {extra}
-
- }
-
- function renderExtra(value, showPercent = false) {
- const cls = value < 0 ? s.negative : (value > 0 ? s.positive : s.neutral)
- const label = showPercent ? percent(value) : value
- return isNumber(value) ? {label} : null
- }
+ useEffect(() => {
+ id && getProject({ id, force: true })
+ id && getIterations({ id })
+ }, [id])
- async function fetchIterations() {
- const result = await func.getIterations(id)
- if (result) {
- const iters = fetchHandle(result)
- setIterations(iters)
+ const fresh = useCallback(project => {
+ if (project) {
+ setProject(project)
+ } else {
+ getProject({ id, force: true })
}
- }
-
- async function fetchProject() {
- const result = await func.getProject(id)
- result && setProject(result)
- }
-
- function fetchHandle(iterations) {
- const iters = iterations.map(iteration => {
- return {
- ...iteration,
- trainUpdateDatasetLabel: renderDatasetLabel(iteration.trainUpdateDataset),
- miningDatasetLabel: renderDatasetLabel(iteration.miningDataset),
- miningResultDatasetLabel: renderDatasetLabel(iteration.miningResultDataset),
- labelDatasetLabel: renderDatasetLabel(iteration.labelDataset),
- testDatasetLabel: renderDatasetLabel(iteration.testDataset),
- map: iteration?.trainingModel?.map,
- }
- })
- iters.reduce((prev, current) => {
- const prevMap = prev.map || 0
- const currentMap = current.map || 0
- const validModels = prev.trainingModel && current.trainingModel
- current.mapEffect = validModels ? (currentMap - prevMap) : null
-
- const validTrainSet = prev.trainUpdateDataset && current.trainUpdateDataset
- const prevUpdatedTrainSetCount = prev?.trainUpdateDataset?.assetCount || 0
- const currentUpdatedTrainSetCount = current?.trainUpdateDataset?.assetCount || 0
- current.trainEffect = validTrainSet ? (currentUpdatedTrainSetCount - prevUpdatedTrainSetCount) : null
-
- return current
- }, {})
- return iters
- }
-
- function renderDatasetLabel(dataset) {
- return dataset ? `${dataset.name} ${dataset.versionName} (${dataset.assetCount})` : ''
- }
-
- function showTitle(str) {
- return {t(str)}
- }
-
- function renderTitle() {
- return (
-
- {project.name} {t('project.iterations.title')}
- history.goBack()}>{t('common.back')}>
-
- )
- }
+ }, [id])
return (
-
-
- {t('project.train_classes')}: {project?.keywords?.join(',')}
-
- {t('project.detail.info.iteration', {
- stageLabel: {t(getStageLabel(project.currentStage, project.round))} ,
- current: {project.round} ,
- })}
-
- {project.description ? {t('project.detail.desc')}: {project.description} : null}
-
-
-
record.id}
- columns={columns}
- >
-
-
+
+
+ {project.round > 0 ?
+
:
}
+
+
)
}
-const props = (state) => {
- return {
- logined: state.user.logined,
- }
-}
-
-const actions = (dispatch) => {
- return {
- getProject(id) {
- return dispatch({
- type: "project/getProject",
- payload: { id },
- })
- },
- getIterations(id) {
- return dispatch({
- type: 'iteration/getIterations',
- payload: { id, more: true },
- })
- }
- }
-}
-export default connect(props, actions)(Iterations)
+export default Iterations
diff --git a/ymir/web/src/pages/project/iterations/buttons.js b/ymir/web/src/pages/project/iterations/buttons.js
new file mode 100644
index 0000000000..6491e7d6be
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/buttons.js
@@ -0,0 +1,58 @@
+import React, { } from "react"
+import { Button, Form, Space } from "antd"
+
+import t from "@/utils/t"
+import { validState, invalidState, readyState } from '@/constants/common'
+
+function Buttons({
+ step,
+ state,
+ next = () => { },
+ skip = () => { },
+ react = () => { },
+}) {
+ const actLabel = t(step.act)
+ const reactLabel = t(step.react)
+ const end = !step.next
+ const finished = step.value < step.current
+ const current = step.value === step.current
+ const pending = step.value > step.current
+
+ const stepPending = state < 0
+ const retry = state === -2
+
+ const skipBtn = !step.unskippable && !end && current ?
+
+ {t('common.skip')}
+
+ : null
+
+ const confirmBtn = current && stepPending ?
+
+ {retry ? reactLabel : actLabel}
+
+ : null
+
+ const nextBtn = retry || (current && validState(state)) ?
+
+ {t('common.step.next')}
+
+ : null
+
+ const reactBtn = current && (validState(state) || invalidState(state)) ?
+
+ {reactLabel}
+
+ : null
+
+ return (
+
+ {confirmBtn}
+ {reactBtn}
+ {nextBtn}
+ {skipBtn}
+
+ )
+}
+
+export default Buttons
diff --git a/ymir/web/src/pages/project/iterations/columns.js b/ymir/web/src/pages/project/iterations/columns.js
new file mode 100644
index 0000000000..8ee8ba706b
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/columns.js
@@ -0,0 +1,104 @@
+import t from '@/utils/t'
+import { Col, Popover, Row, Tooltip } from 'antd'
+import { Link } from 'umi'
+
+import { humanize } from "@/utils/number"
+import { validDataset } from '@/constants/dataset'
+import { percent } from '@/utils/number'
+import { diffTime } from '@/utils/date'
+import { getRecommendStage, validModel } from '@/constants/model'
+
+import { DescPop } from "@/components/common/descPop"
+import TypeTag from "@/components/task/typeTag"
+import RenderProgress from "@/components/common/progress"
+
+function showTitle(str) {
+ return {t(str)}
+}
+
+const nameCol = (type = 'dataset') => ({
+ title: showTitle(`${type}.column.name`),
+ key: "name",
+ dataIndex: "versionName",
+ render: (name, { id, name: groupName, projectId: pid, description }) => {
+ const popContent =
+ const content = {groupName} {name}
+ return description ?
+ {content}
+ : content
+ },
+ ellipsis: true,
+})
+const sourceCol = {
+ title: showTitle("dataset.column.source"),
+ dataIndex: "taskType",
+ render: (type) => ,
+ sorter: (a, b) => a.taskType - b.taskType,
+ ellipsis: true,
+}
+const countCol = {
+ title: showTitle("dataset.column.asset_count"),
+ dataIndex: "assetCount",
+ render: (num) => humanize(num),
+ sorter: (a, b) => a.assetCount - b.assetCount,
+ width: 120,
+}
+const keywordCol = {
+ title: showTitle("dataset.column.keyword"),
+ dataIndex: "keywords",
+ render: (_, record) => {
+ const { gt, pred, } = record
+ const renderLine = (keywords = [], label = 'gt') =>
+
{t(`annotation.${label}`)}:
+ {t('dataset.column.keyword.label', {
+ keywords: keywords.join(', '),
+ total: keywords.length
+ })}
+
+ const label = <>{renderLine(gt?.keywords)}{renderLine(pred?.keywords, 'pred')}>
+ return validDataset(record) ? {label}
: null
+ },
+ ellipsis: {
+ showTitle: false,
+ },
+}
+const stateCol = {
+ title: showTitle('dataset.column.state'),
+ dataIndex: 'state',
+ render: (state, record) => RenderProgress(state, record),
+}
+const createTimeCol = {
+ title: showTitle("dataset.column.create_time"),
+ dataIndex: "createTime",
+ sorter: (a, b) => diffTime(a.createTime, b.createTime),
+ sortDirections: ['ascend', 'descend', 'ascend'],
+ defaultSortOrder: 'descend',
+ width: 180,
+}
+
+const stageCol = {
+ title: showTitle("model.column.stage"),
+ dataIndex: "recommendStage",
+ render: (_, record) => {
+ const stage = getRecommendStage(record)
+ return validModel(record) ?
+
+ {stage?.name}
+ mAP: {percent(stage?.map)}
+
: null
+ },
+ width: 300,
+}
+
+const getColumns = (type) => {
+ const maps = {
+ dataset: [nameCol(), sourceCol, countCol, keywordCol, stateCol, createTimeCol],
+ model: [nameCol('model'), stageCol, sourceCol, stateCol, createTimeCol],
+ }
+ return maps[type]
+}
+
+export default getColumns
diff --git a/ymir/web/src/pages/project/iterations/detail.js b/ymir/web/src/pages/project/iterations/detail.js
new file mode 100644
index 0000000000..fe2cfe528e
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/detail.js
@@ -0,0 +1,56 @@
+import React, { useEffect, useState } from "react"
+import { useSelector } from 'umi'
+
+import t from "@/utils/t"
+import useFetch from '@/hooks/useFetch'
+import Panel from "./detail.panel"
+
+import s from "./index.less"
+
+const filterExsit = list => list.filter(item => item)
+
+function Detail({ project = {} }) {
+
+ const [settings, setSettings] = useState([])
+ const [intermediations, setIntermediations] = useState([])
+ const [models, setModels] = useState([])
+ const [_, getIteration] = useFetch('iteration/getIteration')
+ const iid = project.currentIteration?.id
+ const iteration = useSelector(({ iteration }) => iteration.iteration[iid])
+
+ useEffect(() => {
+ project.id && iid && getIteration({ pid: project.id, id: iid, more: true })
+ }, [project.id, iid])
+
+ useEffect(() => {
+ if (!project.id) {
+ return
+ }
+ setSettings(filterExsit([project.miningSet?.id, project.testSet?.id]))
+ if (!iteration?.id) {
+ return
+ }
+ const {
+ wholeMiningSet,
+ trainUpdateSet,
+ miningSet,
+ miningResult,
+ labelSet,
+ testSet,
+ model,
+ } = iteration || {}
+ setSettings(filterExsit([wholeMiningSet, testSet]))
+ setIntermediations(filterExsit([trainUpdateSet, labelSet, miningResult, miningSet,]))
+ setModels(filterExsit([model]))
+ }, [iteration, project])
+
+ return (
+
+
+ {intermediations.length ?
: null}
+ {models.length ?
: null}
+
+ )
+}
+
+export default Detail
diff --git a/ymir/web/src/pages/project/iterations/detail.panel.js b/ymir/web/src/pages/project/iterations/detail.panel.js
new file mode 100644
index 0000000000..43e953c9cc
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/detail.panel.js
@@ -0,0 +1,43 @@
+import React, { useEffect, useState } from "react"
+import { Table } from "antd"
+import { useSelector } from 'umi'
+
+import t from "@/utils/t"
+
+import s from "./index.less"
+import getColumns from "./columns"
+
+function Panel({ list = [], customColumns, title = '', type = 'dataset' }) {
+
+ const [columns, setColumns] = useState([])
+ const rows = useSelector(({ dataset, model }) => {
+ const isModel = type !== 'dataset'
+ const res = isModel ? model.model: dataset.dataset
+ return list.length ? [...(list.map(id => {
+ const result = res[id]
+ return res[id] ? {
+ ...result,
+ index: `${result.id}${new Date().getTime()}`,
+ } : null
+ }).filter(item => item))] : []
+ })
+
+ useEffect(() => setColumns(getColumns(type)), [type])
+
+ useEffect(() => customColumns && setColumns(customColumns), [customColumns])
+
+ return
+
{title}
+
+
record.index}
+ rowClassName={(_, index) => index % 2 === 0 ? '' : 'oddRow'}
+ pagination={false}
+ />
+
+
+}
+
+export default Panel
diff --git a/ymir/web/src/pages/project/iterations/generateStages.js b/ymir/web/src/pages/project/iterations/generateStages.js
new file mode 100644
index 0000000000..570fade63f
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/generateStages.js
@@ -0,0 +1,38 @@
+
+export default (project = {}) => {
+ if (!project.id) {
+ return []
+ }
+ const datasetStages = [
+ { field: 'candidateTrainSet', option: true, label: 'project.prepare.trainset', tip: 'project.add.trainset.tip', },
+ { field: 'testSet', label: 'project.prepare.validationset', tip: 'project.add.testset.tip', },
+ { field: 'miningSet', label: 'project.prepare.miningset', tip: 'project.add.miningset.tip', },
+ ]
+
+ const modelStage = {
+ field: 'modelStage',
+ label: 'project.prepare.model',
+ tip: 'tip.task.filter.model',
+ type: 1,
+ filter: x => x,
+ }
+
+ const generateFilters = (field, project) => {
+ const notTestingSet = (did) => !(project.testingSets || []).includes(did)
+ const excludeSelected = (currentField, dataset, project) => {
+ const excludeCurrent = datasetStages.filter(({ type, field }) => !type && field !== currentField)
+ const ids = excludeCurrent.map(({ field }) => project[field]?.id || project[field])
+ return !ids.includes(dataset.id)
+ }
+ return (datasets, project) => {
+ return datasets.filter(dataset =>
+ notTestingSet(dataset.id) &&
+ excludeSelected(field, dataset, project)
+ )
+ }
+ }
+ return [
+ ...datasetStages.map((item) => ({ ...item, filter: generateFilters(item.field, project) })),
+ modelStage,
+ ]
+}
diff --git a/ymir/web/src/pages/project/iterations/index.less b/ymir/web/src/pages/project/iterations/index.less
new file mode 100644
index 0000000000..4f877d7f07
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/index.less
@@ -0,0 +1,49 @@
+.header {
+ background: #fff;
+ margin: 0 0 20px;
+ padding: 10px 20px 20px;
+ border-radius: 5px;
+}
+.list {
+ .extraTag {
+ position: absolute;
+ right: 0;
+ top: 2px;
+ font-size: 12px;
+ line-height: 12px;
+ color: #fff;
+ text-align: right;
+ .negative, .positive, .neutral {
+ display: inline-block;
+ padding: 2px 4px;
+ border-radius: 4px;
+ &::after {
+ display: inline-block;
+ margin-left: 4px;
+ }
+ }
+ .negative {
+ background-color: darkred;
+ &::after {
+ content: '\2193';
+ }
+ }
+ .positive {
+ &::after {
+ content: '\2191';
+ }
+ background-color: darkgreen;
+ }
+ .neutral {
+ background-color: darkgray;
+ }
+ }
+}
+.orange {
+ color: orange;
+}
+.title {
+ font-size: 16px;
+ font-weight: bold;
+ margin: 20px 0;
+}
diff --git a/ymir/web/src/pages/project/iterations/iteration.js b/ymir/web/src/pages/project/iterations/iteration.js
new file mode 100644
index 0000000000..782ae1fbf4
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/iteration.js
@@ -0,0 +1,126 @@
+import { useCallback, useEffect, useState } from "react"
+import { Row, Col } from "antd"
+import { useSelector } from "umi"
+
+import { Stages, StageList } from '@/constants/iteration'
+import { templateString } from '@/utils/string'
+import useFetch from '@/hooks/useFetch'
+
+import Stage from './stage'
+import StepAction from "./stepAction"
+import s from "./iteration.less"
+
+function Iteration({ project, fresh = () => { } }) {
+ const iteration = useSelector(({ iteration }) => iteration.iteration[project.currentIteration?.id] || {})
+ const [_, getIteration] = useFetch('iteration/getIteration', {})
+ const [stages, setStages] = useState([])
+ const [prevIteration, getPrevIteration] = useFetch('iteration/getIteration', {})
+ const [createResult, create] = useFetch('iteration/createIteration')
+ const [_u, update] = useFetch('iteration/updateIteration')
+
+ useEffect(() => {
+ if ((project.id && project.currentIteration) || iteration?.needReload) {
+ getIteration({ pid: project.id, id: project.currentIteration?.id, more: true })
+ }
+ }, [project.currentIteration, iteration?.needReload])
+
+ useEffect(() => iteration.prevIteration && getPrevIteration({
+ pid: project.id,
+ id: iteration.prevIteration
+ }), [iteration])
+
+ useEffect(() => {
+ iteration.id && rerenderStages()
+ }, [iteration, prevIteration])
+
+ useEffect(() => createResult && fresh(), [createResult])
+
+ const callback = useCallback(iterationHandle, [iteration])
+
+ function getInitStages() {
+ const stageList = StageList()
+ return stageList.list.map(({ label, value, url, output, input }) => {
+ const slabel = `project.iteration.stage.${label}`
+ return {
+ value: value,
+ label,
+ act: slabel,
+ react: `${slabel}.react`,
+ state: -1,
+ next: stageList[value].next,
+ temp: url,
+ output,
+ input,
+ project,
+ unskippable: [Stages.merging, Stages.training].includes(value),
+ callback,
+ }
+ })
+ }
+
+ function rerenderStages() {
+ const initStages = getInitStages()
+ const ss = initStages.map(stage => {
+ const result = iteration[stage.output]
+ return {
+ ...stage,
+ iterationId: iteration.id,
+ round: iteration.round,
+ current: iteration.currentStage,
+ result,
+ }
+ })
+ setStages(ss)
+ }
+
+ function iterationHandle({ type = 'update', data = {} }) {
+ if (type === 'create') {
+ createIteration(data)
+ } else if (type === 'skip') {
+ skipStage(data)
+ } else {
+ updateIteration(data)
+ }
+ }
+
+ function createIteration() {
+ const params = {
+ iterationRound: project.round + 1,
+ projectId: project.id,
+ prevIteration: iteration.id,
+ testSet: project.testSet.id,
+ miningSet: project.miningSet.id,
+ }
+ create(params)
+ }
+ function updateIteration(data = {}) {
+ const params = {
+ id: iteration.id,
+ ...data,
+ }
+ update(params)
+ }
+ function skipStage({ input, value }) {
+ const params = {
+ id: iteration.id,
+ currentStage: value,
+ [input]: 0,
+ }
+ update(params)
+ }
+ return (
+
+
+ {stages.map((stage) =>
+
+
+ )}
+
+
+
+
+
+ )
+}
+
+export default Iteration
diff --git a/ymir/web/src/pages/project/components/iteration.less b/ymir/web/src/pages/project/iterations/iteration.less
similarity index 89%
rename from ymir/web/src/pages/project/components/iteration.less
rename to ymir/web/src/pages/project/iterations/iteration.less
index cd1ceebc6a..627255f68c 100644
--- a/ymir/web/src/pages/project/components/iteration.less
+++ b/ymir/web/src/pages/project/iterations/iteration.less
@@ -52,4 +52,12 @@
&:hover {
text-decoration: underline;
}
-}
\ No newline at end of file
+}
+.createBtn {
+ text-align: center;
+}
+.uploader {
+ :global(.ant-upload.ant-upload-select) {
+ display: block;
+ }
+}
diff --git a/ymir/web/src/pages/project/iterations/list.js b/ymir/web/src/pages/project/iterations/list.js
new file mode 100644
index 0000000000..9d018db04c
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/list.js
@@ -0,0 +1,168 @@
+import React, { useEffect, useState } from "react"
+import { Table, Popover, } from "antd"
+import { useSelector } from 'umi'
+
+import t from "@/utils/t"
+import { percent, isNumber } from '@/utils/number'
+import useFetch from '@/hooks/useFetch'
+import { validModel } from '@/constants/model'
+import { validDataset } from '@/constants/dataset'
+
+import SampleRates from "@/components/dataset/sampleRates"
+import MiningSampleRates from "@/components/dataset/miningSampleRates"
+
+import s from "./index.less"
+import StateTag from "@/components/task/stateTag"
+
+function List({ project }) {
+ const [iterations, getIterations] = useFetch('iteration/getIterations', [])
+ const [list, setList] = useState([])
+ const datasets = useSelector(({ dataset }) => dataset.dataset)
+ const models = useSelector(({ model }) => model.model)
+
+ useEffect(() => {
+ project?.id && getIterations({ id: project.id, more: true })
+ }, [project])
+
+ useEffect(() => {
+ setList(iterations.length ? fetchHandle(iterations) : [])
+ }, [iterations, datasets, models])
+
+ const columns = [
+ {
+ title: showTitle("iteration.column.round"),
+ dataIndex: "round",
+ render: (round) => (t('iteration.round.label', { round })),
+ },
+ {
+ title: showTitle("iteration.column.premining"),
+ dataIndex: "miningDatasetLabel",
+ render: (label, { id, versionName, miningSet }) => renderPop(label, datasets[miningSet], ),
+ ellipsis: true,
+ },
+ {
+ title: showTitle("iteration.column.mining"),
+ dataIndex: "miningResultDatasetLabel",
+ render: (label, { miningResult }) => renderPop(label, datasets[miningResult]),
+ ellipsis: true,
+ },
+ {
+ title: showTitle("iteration.column.label"),
+ dataIndex: "labelDatasetLabel",
+ render: (label, { labelSet }) => renderPop(label, datasets[labelSet]),
+ align: 'center',
+ ellipsis: true,
+ },
+ {
+ title: showTitle("iteration.column.test"),
+ dataIndex: "testDatasetLabel",
+ render: (label, { testSet }) => renderPop(label, datasets[testSet]),
+ align: 'center',
+ ellipsis: true,
+ },
+ {
+ title: showTitle("iteration.column.merging"),
+ dataIndex: "trainUpdateDatasetLabel",
+ render: (label, { trainEffect, trainUpdateSet }) => renderPop(label, datasets[trainUpdateSet],
+ null, {renderExtra(trainEffect)} ),
+ align: 'center',
+ ellipsis: true,
+ },
+ {
+ title: showTitle("iteration.column.training"),
+ dataIndex: 'map',
+ render: (map, { model, mapEffect }) => {
+ const md = models[model] || {}
+ const label = <>{md.name} {md.versionName} {!validModel(md) ? : null}>
+ return validModel(md) ?
+
+ {label}
+
+ {map >= 0 ? percent(map) : null}
+ {renderExtra(mapEffect, true)}
+
: null
+ },
+ align: 'center',
+ },
+ ]
+
+ function renderPop(label, dataset = {}, ccontent, extra = '') {
+ dataset.project = project
+ const content = ccontent ||
+ return
+ {label}
+ {extra}
+
+ }
+
+ function renderExtra(value, showPercent = false) {
+ const cls = value < 0 ? s.negative : (value > 0 ? s.positive : s.neutral)
+ const label = showPercent ? percent(value) : value
+ return isNumber(value) ? {label} : null
+ }
+
+ function fetchHandle(iterations) {
+ const iters = iterations.map(iteration => {
+ const {
+ trainUpdateSet,
+ miningSet,
+ miningResult,
+ labelSet,
+ testSet,
+ model,
+ } = iteration
+ return {
+ ...iteration,
+ index: `${iteration.id}${new Date().getTime()}`,
+ trainUpdateDatasetLabel: renderDatasetLabel(datasets[trainUpdateSet]),
+ miningDatasetLabel: renderDatasetLabel(datasets[miningSet]),
+ miningResultDatasetLabel: renderDatasetLabel(datasets[miningResult]),
+ labelDatasetLabel: renderDatasetLabel(datasets[labelSet]),
+ testDatasetLabel: renderDatasetLabel(datasets[testSet]),
+ map: models[model]?.map,
+ }
+ })
+ iters.reduce((prev, current) => {
+ const prevMap = prev.map || 0
+ const currentMap = current.map || 0
+ const validModels = prev.model && current.model
+ current.mapEffect = validModels ? (currentMap - prevMap) : null
+
+ const validTrainSet = prev.trainUpdateSet && current.trainUpdateSet
+ const prevUpdatedTrainSetCount = datasets[prev.trainUpdateSet]?.assetCount || 0
+ const currentUpdatedTrainSetCount = datasets[current.trainUpdateSet]?.assetCount || 0
+ current.trainEffect = validTrainSet ? (currentUpdatedTrainSetCount - prevUpdatedTrainSetCount) : null
+
+ return current
+ }, {})
+ return iters.reverse()
+ }
+
+ function renderDatasetLabel(dataset) {
+ if (!dataset) {
+ return
+ }
+ const label = `${dataset.name} ${dataset.versionName} (${dataset.assetCount})`
+ return
+ {label}
+ {!validDataset(dataset) ? : null}
+
+ }
+
+ function showTitle(str) {
+ return {t(str)}
+ }
+
+ return (
+
+
record.index}
+ columns={columns}
+ >
+
+ )
+}
+
+export default List
diff --git a/ymir/web/src/pages/project/iterations/nextIteration.js b/ymir/web/src/pages/project/iterations/nextIteration.js
new file mode 100644
index 0000000000..0e5cf9ea1f
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/nextIteration.js
@@ -0,0 +1,9 @@
+import { Col, Form, Row } from "antd"
+
+export default function NextIteration({ok = () => {}, bottom}) {
+ const [form] = Form.useForm()
+
+ return
+ {bottom}
+
+}
\ No newline at end of file
diff --git a/ymir/web/src/pages/project/iterations/prepare.js b/ymir/web/src/pages/project/iterations/prepare.js
new file mode 100644
index 0000000000..1061aa5764
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/prepare.js
@@ -0,0 +1,152 @@
+import { useCallback, useEffect, useState } from "react"
+import { Row, Col, Form, Button } from "antd"
+import { useLocation, useSelector } from 'umi'
+
+import t from '@/utils/t'
+import useFetch from '@/hooks/useFetch'
+import { validDataset } from '@/constants/dataset'
+
+import s from "./iteration.less"
+import Stage from "./prepareStage"
+import generateStages from "./generateStages"
+
+function Prepare({ project, fresh = () => { } }) {
+ const location = useLocation()
+ const [validPrepare, setValidPrepare] = useState(false)
+ const [id, setId] = useState(null)
+ const [stages, setStages] = useState([])
+ const [result, updateProject] = useFetch('project/updateProject')
+ const [mergeResult, merge] = useFetch('task/merge', null, true)
+ const [createdResult, createIteration] = useFetch('iteration/createIteration')
+ const [_, getPrepareStagesResult] = useFetch('iteration/getPrepareStagesResult', {})
+ const results = useSelector(({ dataset, model }) => {
+ const candidateTrainSet = dataset.dataset[project?.candidateTrainSet]
+ const testSet = dataset.dataset[project?.testSet?.id]
+ const miningSet = dataset.dataset[project?.miningSet?.id]
+ const modelStage = model.model[project?.model]
+ return {
+ candidateTrainSet,
+ testSet,
+ miningSet,
+ modelStage,
+ }
+ })
+ const [trainValid, setTrainValid] = useState(false)
+ const [form] = Form.useForm()
+
+ useEffect(() => {
+ project?.id && setId(project.id)
+ project?.id && getPrepareStagesResult({ id: project?.id })
+ }, [project])
+
+ useEffect(() => {
+ if (project?.id) {
+ setStages(generateStages(project))
+ }
+ }, [project?.id])
+
+ useEffect(() => updatePrepareStatus(), [stages, results])
+
+ useEffect(() => {
+ if (result) {
+ fresh(result)
+ updatePrepareStatus()
+ }
+ }, [result])
+
+ useEffect(() => {
+ if (mergeResult) {
+ updateAndCreateIteration(mergeResult.id)
+ }
+ }, [mergeResult])
+
+ useEffect(() => {
+ if (createdResult) {
+ fresh(createdResult)
+ window.location.reload()
+ }
+ }, [createdResult])
+
+ useEffect(() => {
+ setTrainValid([
+ results?.candidateTrainSet,
+ results?.testSet,
+ ].reduce((prev, curr) => prev && validDataset(curr), true))
+ }, [results])
+
+ const updateSettings = (value) => {
+ const target = Object.keys(value).reduce((prev, curr) => ({
+ ...prev,
+ [curr]: value[curr] || null
+ }), {})
+ updateProject({ id, ...target })
+ }
+
+ function create() {
+ const params = {
+ iterationRound: 1,
+ projectId: id,
+ prevIteration: 0,
+ testSet: project?.testSet?.id,
+ miningSet: project?.miningSet?.id,
+ }
+ createIteration(params)
+ }
+
+ async function updateAndCreateIteration(trainSetVersion) {
+ const updateResult = await updateProject({ id, trainSetVersion })
+ if (updateResult) {
+ create()
+ }
+ }
+
+ function mergeTrainSet() {
+ const params = {
+ projectId: id,
+ group: project.trainSet.id,
+ datasets: [project.candidateTrainSet, project.trainSetVersion],
+ }
+ merge(params)
+ }
+
+ function updatePrepareStatus() {
+ const fields = stages.filter(stage => !stage.option).map(stage => stage.field)
+ const valid = fields.every(field => (project[field]?.id || project[field]) && validDataset(results[field]))
+ setValidPrepare(valid)
+ }
+
+ function start() {
+ if (project.candidateTrainSet) {
+ mergeTrainSet()
+ } else {
+ create()
+ }
+ }
+
+ return (
+
+
+
+ {stages.map((stage, index) => (
+
+
+
+ ))}
+
+
+ {t('project.prepare.start')}
+
+
+
+ )
+}
+
+export default Prepare
diff --git a/ymir/web/src/pages/project/iterations/prepareStage.js b/ymir/web/src/pages/project/iterations/prepareStage.js
new file mode 100644
index 0000000000..bc61a0d2a9
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/prepareStage.js
@@ -0,0 +1,101 @@
+import { useCallback, useEffect, useMemo, useState } from "react"
+import { Row, Col, Form, Button, Select } from "antd"
+import { useHistory } from "umi"
+
+import { runningDataset } from '@/constants/dataset'
+import t from '@/utils/t'
+import useFetch from '@/hooks/useFetch'
+import DatasetSelect from '@/components/form/datasetSelect'
+import ModelSelect from '@/components/form/modelSelect'
+
+import { AddIcon, TrainIcon, ImportIcon } from "@/components/common/icons"
+import RenderProgress from "@/components/common/progress"
+
+const SettingsSelection = (Select) => {
+ const Selection = (props) => {
+ return
+ }
+ return Selection
+}
+
+export default function Stage({ pid, stage, form, project = {}, result, trainValid, update }) {
+ const history = useHistory()
+ const [value, setValue] = useState(null)
+ const [valid, setValid] = useState(false)
+ const Selection = useMemo(() => SettingsSelection(stage.type ? ModelSelect : DatasetSelect), [stage.type])
+ const [candidateList, setCandidateList] = useState(true)
+
+ useEffect(() => {
+ setValid(value)
+ }, [value])
+
+ useEffect(() => {
+ const value = getAttrFromProject(stage.field, project)
+ setValue(value)
+ setFieldValue(value)
+ }, [stage, project])
+
+ const setFieldValue = value => form.setFieldsValue({
+ [stage.field]: value || null,
+ })
+
+ const goTraining = () => {
+ const iparams = `from=iteration`
+ history.push(`/home/project/${pid}/train?did=${project.candidateTrainSet}&test=${project.testSet.id}&${iparams}`)
+ }
+
+ const filters = stage.filter ?
+ useCallback(datasets =>
+ stage.filter(datasets, project),
+ [stage.field, project]) : null
+
+ function onSelectionReady(list = []) {
+ const candidateList = stage.filter(list, project)
+ const candidated = !stage.type ?
+ candidateList.some(({ assetCount }) => assetCount) :
+ !!candidateList.length
+ setCandidateList(candidated)
+ }
+
+ const renderEmptyState = (type) => !type ?
+ }
+ onClick={() => history.push(`/home/project/${pid}/dataset/add?from=iteration&stepKey=${stage.field}`)}
+ >{t(`${stage.label}.upload`)}
+ :
+
+
+ {t("project.iteration.stage.training")}
+
+
+
+ history.push(`/home/project/${pid}/model/import?from=iteration&stepKey=${stage.field}`)}>
+ {t("model.import.label")}
+
+
+
+
+ const running =
+
+ return
+ {runningDataset(result) ? running : <>
+ {!candidateList && !project[stage.field] ? renderEmptyState(stage.type) : null}
+
+
+ >}
+ {runningDataset(result) ? {RenderProgress(result?.state, result, true)}
: null}
+
+}
+
+function getAttrFromProject(field, project = {}) {
+ const attr = project[field]
+ return attr?.id ? attr.id : attr
+}
diff --git a/ymir/web/src/pages/project/iterations/stage.js b/ymir/web/src/pages/project/iterations/stage.js
new file mode 100644
index 0000000000..2c408c43d6
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/stage.js
@@ -0,0 +1,71 @@
+import { Col, Popover, Row } from "antd"
+import { useSelector } from "umi"
+
+import t from '@/utils/t'
+import { states, statesLabel } from '@/constants/dataset'
+import s from './iteration.less'
+import { Stages } from "@/constants/iteration"
+import { useEffect, useState } from "react"
+import RenderProgress from "../../../components/common/progress"
+import { YesIcon } from '@/components/common/icons'
+
+function Stage({ stage, end = false, }) {
+ const result = useSelector(({ dataset, model }) => {
+ const isModel = stage.value === Stages.training
+ const res = isModel ? model.model : dataset.dataset
+ return { ...res[stage.result] } || {}
+ })
+ const [state, setState] = useState(-1)
+
+ useEffect(() => {
+ const st = typeof result?.state !== 'undefined' ? result.state : stage.state
+ setState(st)
+ }, [result?.state, stage])
+
+ const currentStage = () => stage.value === stage.current
+ const finishStage = () => stage.value < stage.current
+ const pendingStage = () => stage.value > stage.current
+
+ const isPending = () => state < 0
+ const isReady = () => state === states.READY
+ const isValid = () => state === states.VALID
+ const isInvalid = () => state === states.INVALID
+
+ const stateClass = `${s.stage} ${currentStage() ? s.current : (finishStage() ? s.finish : s.pending)}`
+
+ const renderCount = () => {
+ const content = finishStage() || (currentStage() && isValid()) ? : stage.value + 1
+ const cls = pendingStage() ? s.pending : (currentStage() ? s.current : s.finish)
+ return {content}
+ }
+
+ const renderState = () => {
+ const pendingLabel = 'project.stage.state.pending'
+ const valid = result.name ? `${result.name} ${result.versionName}` : (end ? null : t('common.done'))
+ const currentPending = t('project.stage.state.pending.current')
+ const currentState = isReady() ? RenderProgress(state, result, true) : t(statesLabel(state))
+ const notValid = {isPending() && currentStage() ? currentPending : currentState}
+ const pending = {t(pendingLabel)}
+ return !pendingStage() ? (isValid() ? valid : notValid) : pending
+ }
+
+ return (
+
+
+ {renderCount()}
+
+ {t(stage.act)}
+
+ {!end ? : null}
+
+
+
+
+ {renderState()}
+
+
+
+ )
+}
+
+export default Stage
diff --git a/ymir/web/src/pages/project/iterations/stepAction.js b/ymir/web/src/pages/project/iterations/stepAction.js
new file mode 100644
index 0000000000..3d3cfc06e0
--- /dev/null
+++ b/ymir/web/src/pages/project/iterations/stepAction.js
@@ -0,0 +1,154 @@
+import { useEffect, useState } from 'react'
+import { useSelector } from 'umi'
+import { Space } from 'antd'
+
+import { Stages, StageList } from '@/constants/iteration'
+import useFetch from '@/hooks/useFetch'
+
+import Fusion from "@/components/task/fusion"
+import Mining from "@/components/task/mining"
+import Label from "@/components/task/label"
+import Merge from "@/components/task/merge"
+import Training from "@/components/task/training"
+import Buttons from './buttons'
+import NextIteration from './nextIteration'
+
+const Action = (Comp, props = {}) =>
+
+const StepAction = ({ stages, iteration, project, prevIteration, callback = () => {} }) => {
+ const [updated, updateIteration] = useFetch('iteration/updateIteration')
+ const actionPanelExpand = useSelector(({iteration}) => iteration.actionPanelExpand)
+ const [currentContent, setCurrentContent] = useState(null)
+ const [CurrentAction, setCurrentAction] = useState(null)
+ const result = useSelector(({ dataset, model }) => {
+ const isModel = currentContent?.value === Stages.training
+ const res = isModel ? model.model : dataset.dataset
+ return res[currentContent?.result] || {}
+ })
+ const [state, setState] = useState(-1)
+
+ const comps = {
+ [Stages.prepareMining]: {
+ comp: Fusion, query: {
+ did: project.miningSet?.id,
+ strategy: project.miningStrategy,
+ chunk: project.chunkSize || undefined,
+ },
+ },
+ [Stages.mining]: {
+ comp: Mining, query: {
+ did: iteration.miningSet,
+ mid: prevIteration.id ? [prevIteration.model, null] : project.modelStage,
+ },
+ },
+ [Stages.labelling]: {
+ comp: Label, query: {
+ did: iteration.miningResult,
+ },
+ },
+ [Stages.merging]: {
+ comp: Merge, query: {
+ did: prevIteration.trainUpdateSet || project.trainSetVersion,
+ mid: iteration.labelSet ? [iteration.labelSet] : undefined,
+ },
+ },
+ [Stages.training]: {
+ comp: Training, query: {
+ did: iteration.trainUpdateSet,
+ test: iteration.testSet,
+ },
+ },
+ [Stages.next]: {
+ comp: NextIteration, query: {}
+ },
+ }
+ const fixedQuery = {
+ iterationId: iteration.id,
+ currentStage: iteration.currentStage,
+ from: 'iteration'
+ }
+
+ useEffect(() => {
+ if (currentContent) {
+ const bottom =
+ const props = {
+ bottom,
+ step: currentContent,
+ hidden: state >= 0,
+ query: { ...fixedQuery, ...currentContent.query },
+ ok,
+ }
+ setCurrentAction(Action(currentContent.comp, props))
+ }
+ }, [currentContent, state])
+
+ useEffect(() => {
+ if (currentContent) {
+ const state = result?.id ? result.state : currentContent.state
+ setState(state)
+ }
+ }, [result?.state, currentContent?.state])
+
+ useEffect(() => {
+ if (!stages.length) {
+ return
+ }
+ const targetStage = stages.find(({ value }) => value === iteration.currentStage)
+ setCurrentContent({
+ ...targetStage,
+ ...comps[iteration.currentStage],
+ })
+ }, [iteration?.currentStage, stages])
+
+ useEffect(() => {
+ if (updated) {
+ message.info(t('task.fusion.create.success.msg'))
+ clearCache()
+ history.replace(`/home/project/${pid}/iterations`)
+ }
+ }, [updated])
+
+ const react = () => {
+ setState(-2)
+ }
+
+ const next = () => {
+ // next
+ callback({
+ type: 'update',
+ data: {
+ currentStage: currentContent.next.value,
+ },
+ })
+ }
+
+ const skip = () => {
+ // skip
+ callback({
+ type: 'skip',
+ data: currentContent.next,
+ })
+ }
+
+ const ok = (result) => {
+ if (!currentContent.next) {
+ // next iteration
+ callback({
+ type: 'create',
+ })
+ } else {
+ // update current stage
+ callback({
+ type: 'update',
+ data: {
+ currentStage: currentContent.value,
+ [currentContent.output]: result.id,
+ }
+ })
+ }
+ }
+
+ return {CurrentAction}
+}
+
+export default StepAction
diff --git a/ymir/web/src/pages/project/models.js b/ymir/web/src/pages/project/models.js
new file mode 100644
index 0000000000..cabc6ce580
--- /dev/null
+++ b/ymir/web/src/pages/project/models.js
@@ -0,0 +1,6 @@
+import Models from '@/components/model/list'
+import ListHOC from "./components/list.hoc"
+
+const List = ListHOC(Models)
+
+export default () =>
diff --git a/ymir/web/src/pages/task/compare/components/keywordSelect.js b/ymir/web/src/pages/task/compare/components/keywordSelect.js
index e0c794e0ee..d53b4fcd17 100644
--- a/ymir/web/src/pages/task/compare/components/keywordSelect.js
+++ b/ymir/web/src/pages/task/compare/components/keywordSelect.js
@@ -12,7 +12,7 @@ const KeywordSelect = ({ value, keywords, onChange = () => {} }) => {
if (keywords?.length) {
setOptions([
...keywords.map(label => ({ key: label, label: label })),
- { key: '', label: t('common.everage') }
+ { key: '', label: t('common.average') }
])
} else {
setOptions([])
@@ -37,7 +37,7 @@ const KeywordSelect = ({ value, keywords, onChange = () => {} }) => {
{t('dataset.column.keyword')}:
- {selected === '' ? t('common.everage') : selected}
+ {selected === '' ? t('common.average') : selected}
diff --git a/ymir/web/src/pages/task/compare/index.js b/ymir/web/src/pages/task/compare/index.js
index ccba9651e6..b2a23f477c 100644
--- a/ymir/web/src/pages/task/compare/index.js
+++ b/ymir/web/src/pages/task/compare/index.js
@@ -129,7 +129,7 @@ function Compare({ ...func }) {
title: t("dataset.column.name"),
dataIndex: "name",
render: (name, { id }) => {
- const extra = id === gt.id ? Ground Truth : null
+ const extra = id === gt.id ? {t('annotation.gt')} : null
return <>{name} {extra}>
},
ellipsis: {
diff --git a/ymir/web/src/pages/task/components/useSubmitHandle.js b/ymir/web/src/pages/task/components/useSubmitHandle.js
new file mode 100644
index 0000000000..88347e3e34
--- /dev/null
+++ b/ymir/web/src/pages/task/components/useSubmitHandle.js
@@ -0,0 +1,20 @@
+import { useHistory, useParams } from 'umi'
+import useFetch from '@/hooks/useFetch'
+
+function useSubmitHandle(type = 'dataset') {
+ const history = useHistory()
+ const { id: pid } = useParams()
+ const [_d, clearDatasetCache] = useFetch('dataset/clearCache')
+ const [_m, clearModelCache] = useFetch('model/clearCache')
+
+ const handle = (result = {}) => {
+ const group =(result[`result_${type}`] || result || {})[`${type}_group_id`] || result.id
+ let redirect = `/home/project/${pid}/${type}#${group || ''}`
+ history.replace(redirect)
+ clearModelCache()
+ clearDatasetCache()
+ }
+ return handle
+}
+
+export default useSubmitHandle
diff --git a/ymir/web/src/pages/task/copy/index.js b/ymir/web/src/pages/task/copy/index.js
index 5d520cb497..79d48e97ff 100644
--- a/ymir/web/src/pages/task/copy/index.js
+++ b/ymir/web/src/pages/task/copy/index.js
@@ -8,7 +8,8 @@ import t from "@/utils/t"
import { randomNumber } from "@/utils/number"
import Breadcrumbs from "@/components/common/breadcrumb"
import commonStyles from "../common.less"
-import Tip from "@/components/form/tip"
+import Desc from "@/components/form/desc"
+import DatasetName from "@/components/form/items/datasetName"
function Copy({ allDatasets, datasetCache, ...props }) {
const pageParams = useParams()
@@ -42,7 +43,8 @@ function Copy({ allDatasets, datasetCache, ...props }) {
if (result) {
message.success(t('dataset.copy.success.msg'))
props.clearCache()
- history.replace(`/home/project/detail/${pid}`)
+ const group = result.dataset_group_id || ''
+ history.replace(`/home/project/${pid}/dataset#${group}`)
}
}
@@ -63,47 +65,23 @@ function Copy({ allDatasets, datasetCache, ...props }) {
labelAlign={'left'}
colon={false}
>
-
- {dataset.name} {dataset.versionName} (assets: {dataset.assetCount})
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('common.action.copy')}
-
-
-
- history.goBack()}>
- {t('task.btn.back')}
-
-
-
-
-
+ {dataset.name} {dataset.versionName} (assets: {dataset.assetCount})
+
+
+
+
+
+
+ {t('common.action.copy')}
+
+
+
+ history.goBack()}>
+ {t('task.btn.back')}
+
+
+
+
diff --git a/ymir/web/src/pages/task/filter/index.js b/ymir/web/src/pages/task/filter/index.js
new file mode 100644
index 0000000000..87b8c8e0e9
--- /dev/null
+++ b/ymir/web/src/pages/task/filter/index.js
@@ -0,0 +1,138 @@
+import React, { useState, useEffect, useCallback } from "react"
+import { Button, Form, message, Card, Space, InputNumber } from "antd"
+import { useHistory, useLocation, useParams } from "umi"
+
+import { formLayout } from "@/config/antd"
+import t from "@/utils/t"
+import useFetch from '@/hooks/useFetch'
+
+import Breadcrumbs from "@/components/common/breadcrumb"
+import RecommendKeywords from "@/components/common/recommendKeywords"
+import Desc from "@/components/form/desc"
+import KeywordSelect from "@/components/form/keywordSelect"
+
+import commonStyles from "../common.less"
+import s from "./index.less"
+
+function Filter() {
+ const { query } = useLocation()
+ const did = Number(query.did)
+ const history = useHistory()
+ const [form] = Form.useForm()
+ const [keywords, setKeywords] = useState([])
+ const [dataset, getDataset] = useFetch('dataset/getDataset', {})
+ const [filterResult, filter] = useFetch('task/filter')
+ const [_, clearCache] = useFetch('dataset/clearCache')
+ const includes = Form.useWatch('includes', form)
+ const excludes = Form.useWatch('excludes', form)
+
+ useEffect(() => {
+ did && getDataset({ id: did })
+ }, [did])
+
+ useEffect(() => {
+ if (dataset?.id) {
+ setKeywords(dataset.keywords)
+ }
+ }, [dataset])
+
+ useEffect(() => {
+ if (filterResult) {
+ message.info(t('task.fusion.create.success.msg'))
+ clearCache()
+ const group = filterResult.dataset_group_id || ''
+ history.replace(`/home/project/${dataset.projectId}/dataset#${group}`)
+ }
+ }, [filterResult])
+
+ const checkInputs = (i) => {
+ return i.excludes?.length || i.includes?.length || i.samples
+ }
+
+ const onFinish = (values) => {
+ if (!checkInputs(values)) {
+ return message.error(t('dataset.filter.validate.inputs'))
+ }
+ const params = {
+ ...values,
+ projectId: dataset.projectId,
+ dataset: did,
+ }
+ filter(params)
+ }
+
+ const onFinishFailed = (err) => {
+ console.log("on finish failed: ", err)
+ }
+
+ function selectRecommendKeywords(keyword) {
+ const kws = [...new Set([...(includes || []), keyword])]
+ form.setFieldsValue({ includes: kws })
+ }
+
+ const filterExcludes = useCallback(options => {
+ return options.filter(({ value }) => !includes || !(includes || []).includes(value))
+ }, [includes])
+
+ const filterIncludes = useCallback(options => {
+ return options.filter(({ value }) => !excludes || !(excludes || []).includes(value))
+ }, [excludes])
+
+ return (
+
+
+
+
+
+ {dataset.name} {dataset.versionName} (assets: {dataset.assetCount})
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('common.confirm')}
+
+
+
+ history.goBack()}>
+ {t('task.btn.back')}
+
+
+
+
+
+
+
+ )
+}
+
+export default Filter
diff --git a/ymir/web/src/pages/task/filter/index.less b/ymir/web/src/pages/task/filter/index.less
new file mode 100644
index 0000000000..543816fbb5
--- /dev/null
+++ b/ymir/web/src/pages/task/filter/index.less
@@ -0,0 +1,18 @@
+.dataset {
+ margin: 0 0 0 30px;
+}
+.keyword {
+ border: 1px solid #ccc;
+ font-size: 16px;
+ padding: 4px 12px;
+ margin-bottom: 10px;
+}
+.classics {
+ display: flex;
+ width: 80%;
+ text-align: left;
+ margin: auto;
+}
+.submit {
+ // margin: 50px 0 0 25%;
+}
diff --git a/ymir/web/src/pages/task/fusion/index.js b/ymir/web/src/pages/task/fusion/index.js
index 4a5e2b6e1b..b34d2d2a80 100644
--- a/ymir/web/src/pages/task/fusion/index.js
+++ b/ymir/web/src/pages/task/fusion/index.js
@@ -1,338 +1,21 @@
-import React, { useState, useEffect, memo, useMemo } from "react"
-import { connect } from "dva"
-import { Input, Select, Button, Form, message, ConfigProvider, Card, Space, Radio, Row, Col, InputNumber, Checkbox } from "antd"
-import { useHistory, useLocation, useParams } from "umi"
+import { useLocation } from "umi"
-import { formLayout } from "@/config/antd"
-import t from "@/utils/t"
-import { randomNumber } from "@/utils/number"
-import { MiningStrategy } from '@/constants/project'
-import Breadcrumbs from "@/components/common/breadcrumb"
-import EmptyState from '@/components/empty/dataset'
-import s from "./index.less"
-import commonStyles from "../common.less"
-import Tip from "@/components/form/tip"
-import RecommendKeywords from "@/components/common/recommendKeywords"
-import Panel from "@/components/form/panel"
-import DatasetSelect from "@/components/form/datasetSelect"
-const { Option } = Select
+import Breadcrumbs from "@/components/common/breadcrumb"
+import Fusion from "@/components/task/fusion"
+import useSubmitHandle from "../components/useSubmitHandle"
-function Fusion({ allDatasets, datasetCache, ...func }) {
- const pageParams = useParams()
- const pid = Number(pageParams.id)
+function FusionIndex() {
const { query } = useLocation()
- const { iterationId, currentStage, outputKey, chunk, strategy = '', merging } = query
- const did = Number(query.did)
- const history = useHistory()
- const [form] = Form.useForm()
- const [dataset, setDataset] = useState({})
- const [datasets, setDatasets] = useState([])
- const [includeDatasets, setIncludeDatasets] = useState([])
- const [excludeDatasets, setExcludeDatasets] = useState([])
- const [miningStrategy, setMiningStrategy] = useState(strategy || 0)
- const [excludeResult, setExcludeResult] = useState(strategy === '' ? false : true)
- const [keywords, setKeywords] = useState([])
- const [selectedKeywords, setSelectedKeywords] = useState([])
- const [selectedExcludeKeywords, setExcludeKeywords] = useState([])
- const [visibles, setVisibles] = useState({
- merge: true,
- filter: true,
- sampling: true,
- })
-
- const initialValues = {
- name: 'task_fusion_' + randomNumber(),
- samples: chunk,
- include_datasets: Number(merging) ? [Number(merging)] : [],
- strategy: 2,
- }
-
- useEffect(() => {
- pid && func.getDatasets(pid)
- }, [pid])
-
- useEffect(() => {
- did && func.getDataset(did)
- }, [did])
-
- useEffect(() => {
- const dst = datasetCache[did]
- dst && setDataset(dst)
- }, [datasetCache])
-
- useEffect(() => {
- getKeywords()
- }, [datasets, includeDatasets])
-
- useEffect(() => {
- setDatasets(allDatasets)
- }, [allDatasets])
-
- useEffect(() => {
- const state = history.location.state
-
- if (state?.record) {
- const { parameters, name, } = state.record
- const { include_classes, include_datasets, exclude_classes, include_strategy } = parameters
- //do somethin
- form.setFieldsValue({
- name: `${name}_${randomNumber()}`,
- datasets: include_datasets,
- inc: include_classes,
- exc: exclude_classes,
- strategy: include_strategy,
- })
- setSelectedKeywords(include_classes)
- setExcludeKeywords(exclude_classes)
- history.replace({ state: {} })
- }
- }, [history.location.state])
-
- const getKeywords = () => {
- const selectedDataset = [did, ...includeDatasets]
- let ks = datasets.reduce((prev, current) => selectedDataset.includes(current.id)
- ? prev.concat(current.keywords)
- : prev, [])
- ks = [...new Set(ks)]
- ks.sort()
- setKeywords(ks)
- }
-
- const checkInputs = (i) => {
- return i.exc || i.inc || i.samples || i?.exclude_datasets?.length || i?.include_datasets?.length
- }
-
- const onFinish = async (values) => {
- if(!checkInputs(values)) {
- return message.error(t('dataset.fusion.validate.inputs'))
- }
- const params = {
- ...values,
- project_id: dataset.projectId,
- group_id: dataset.groupId,
- dataset: did,
- include: selectedKeywords,
- exclude: selectedExcludeKeywords,
- mining_strategy: miningStrategy,
- exclude_result: excludeResult,
- include_strategy: Number(values.strategy) || 2,
- }
- if (iterationId) {
- params.iteration = iterationId
- params.stage = currentStage
- }
- const result = await func.createFusionTask(params)
- if (result) {
- if (iterationId) {
- func.updateIteration({ id: iterationId, currentStage, [outputKey]: result.id })
- }
- message.info(t('task.fusion.create.success.msg'))
- func.clearCache()
- history.replace(`/home/project/detail/${dataset.projectId}`)
- }
- }
-
- const onFinishFailed = (err) => {
- console.log("on finish failed: ", err)
- }
-
- function onIncludeDatasetChange(values) {
- setIncludeDatasets(values)
-
- getKeywords()
- // reset
- setSelectedKeywords([])
- setExcludeKeywords([])
- form.setFieldsValue({ inc: [], exc: [] })
- }
- function onExcludeDatasetChange(values) {
- setExcludeDatasets(values)
- // todo inter keywords
- }
-
- function miningStrategyChanged({ target: { checked } }) {
- if (Number(strategy) === MiningStrategy.free) {
- setMiningStrategy(checked ? MiningStrategy.unique : MiningStrategy.free)
- setExcludeResult(true)
- } else {
- setExcludeResult(checked)
- }
- }
-
- function selectRecommendKeywords(keyword) {
- const kws = [...new Set([...selectedKeywords, keyword])]
- setSelectedKeywords(kws)
- form.setFieldsValue({ inc: kws })
- }
-
+ const submitHandle = useSubmitHandle()
return (
-
+
-
-
-
-
- {dataset.name} {dataset.versionName} (assets: {dataset.assetCount})
-
-
- setVisibles(old => ({ ...old, merge: value }))}>
- history.push(`/home/dataset/add/${dataset.projectId}`)} />}>
-
-
-
-
-
-
-
-
-
-
- {strategy.length ?
-
-
-
- {t(`project.mining.strategy.${strategy}.label`)}
-
-
-
- : null}
-
-
-
-
-
-
-
- setVisibles(old => ({ ...old, filter: value }))}>
-
- }
- >
- setSelectedKeywords(value)}
- showArrow
- >
- {keywords.map(keyword => selectedExcludeKeywords.indexOf(keyword) < 0
- ? {keyword}
- : null)}
-
-
-
-
-
- setExcludeKeywords(value)}
- showArrow
- >
- {keywords.map(keyword => selectedKeywords.indexOf(keyword) < 0
- ? {keyword}
- : null)}
-
-
-
-
- setVisibles(old => ({ ...old, sampling: value }))}>
-
-
-
-
-
-
-
-
-
-
-
- {t('common.confirm')}
-
-
-
- history.goBack()}>
- {t('task.btn.back')}
-
-
-
-
-
-
+
+
)
}
-const props = (state) => {
- return {
- allDatasets: state.dataset.allDatasets,
- datasetCache: state.dataset.dataset,
- }
-}
-const mapDispatchToProps = (dispatch) => {
- return {
- getDatasets(pid, force = true) {
- return dispatch({
- type: "dataset/queryAllDatasets",
- payload: { pid, force },
- })
- },
- getDataset(id, force) {
- return dispatch({
- type: "dataset/getDataset",
- payload: { id, force },
- })
- },
- clearCache() {
- return dispatch({ type: "dataset/clearCache", })
- },
- createFusionTask(payload) {
- return dispatch({
- type: "task/createFusionTask",
- payload,
- })
- },
- updateIteration(params) {
- return dispatch({
- type: 'iteration/updateIteration',
- payload: params,
- })
- },
- }
-}
-
-export default connect(props, mapDispatchToProps)(Fusion)
+export default FusionIndex
diff --git a/ymir/web/src/pages/task/inference/index.js b/ymir/web/src/pages/task/inference/index.js
index 8e354f4c13..73cab81eb9 100644
--- a/ymir/web/src/pages/task/inference/index.js
+++ b/ymir/web/src/pages/task/inference/index.js
@@ -1,33 +1,40 @@
import React, { useCallback, useEffect, useState } from "react"
import { connect } from "dva"
-import { Select, Card, Input, Radio, Button, Form, Row, Col, ConfigProvider, Space, InputNumber, message, Tag, Alert } from "antd"
-import {
- PlusOutlined,
- MinusCircleOutlined,
- UpSquareOutlined,
- DownSquareOutlined,
-} from '@ant-design/icons'
-import styles from "./index.less"
-import commonStyles from "../common.less"
-import { formLayout } from "@/config/antd"
+import { Select, Card, Button, Form, Row, Col, Space, InputNumber, message, Tag, Alert } from "antd"
+import { useHistory, useParams, useLocation } from "umi"
+import { formLayout } from "@/config/antd"
import t from "@/utils/t"
+import { HIDDENMODULES } from '@/constants/common'
import { string2Array } from "@/utils/string"
+import { OPENPAI_MAX_GPU_COUNT } from '@/constants/common'
import { TYPES } from '@/constants/image'
-import { useHistory, useParams, useLocation } from "umi"
+import useFetch from '@/hooks/useFetch'
+
import Breadcrumbs from "@/components/common/breadcrumb"
-import EmptyStateDataset from '@/components/empty/dataset'
-import EmptyStateModel from '@/components/empty/model'
import { randomNumber } from "@/utils/number"
-import Tip from "@/components/form/tip"
import ModelSelect from "@/components/form/modelSelect"
import ImageSelect from "@/components/form/imageSelect"
import DatasetSelect from "@/components/form/datasetSelect"
import useAddKeywords from "@/hooks/useAddKeywords"
import AddKeywordsBtn from "@/components/keyword/addKeywordsBtn"
+import LiveCodeForm from "@/components/form/items/liveCode"
+import { removeLiveCodeConfig } from "@/components/form/items/liveCodeConfig"
+import DockerConfigForm from "@/components/form/items/dockerConfig"
+import Desc from "@/components/form/desc"
+
+import commonStyles from "../common.less"
+import styles from "./index.less"
+import OpenpaiForm from "@/components/form/items/openpai"
+import Tip from "@/components/form/tip"
const { Option } = Select
+const getArray = (str = '') => str.split('|')
+const parseModelStage = (str = '') => {
+ return str ? getArray(str).map(stage => string2Array(stage)) : []
+}
+
const Algorithm = () => [{ id: "aldd", label: 'ALDD', checked: true }]
function Inference({ datasetCache, datasets, ...func }) {
@@ -35,50 +42,67 @@ function Inference({ datasetCache, datasets, ...func }) {
const pid = Number(pageParams.id)
const history = useHistory()
const location = useLocation()
- const { did, image } = location.query
- const mid = string2Array(location.query.mid) || []
- const [dataset, setDataset] = useState({})
+ const { image } = location.query
+ const stage = parseModelStage(location.query.mid)
const [selectedModels, setSelectedModels] = useState([])
- const [gpuStep, setGpuStep] = useState(1)
const [form] = Form.useForm()
- const [seniorConfig, setSeniorConfig] = useState([])
- const [hpVisible, setHpVisible] = useState(false)
+ const [seniorConfig, setSeniorConfig] = useState({})
const [gpu_count, setGPU] = useState(0)
+ const [taskCount, setTaskCount] = useState(1)
const [selectedGpu, setSelectedGpu] = useState(0)
const [keywordRepeatTip, setKRTip] = useState('')
- const [{ newer }, checkKeywords] = useAddKeywords(true)
+ const [{ newer }, checkKeywords] = useFetch('keyword/checkDuplication', { newer: [] })
+ const [live, setLiveCode] = useState(false)
+ const [project, getProject] = useFetch('project/getProject', {})
+ const watchStages = Form.useWatch('stages', form)
+ const watchTestingSets = Form.useWatch('datasets', form)
+ const [openpai, setOpenpai] = useState(false)
+ const [sys, getSysInfo] = useFetch('common/getSysInfo', {})
+ const selectOpenpai = Form.useWatch('openpai', form)
+ const [showConfig, setShowConfig] = useState(false)
useEffect(() => {
- fetchSysInfo()
+ getSysInfo()
}, [])
useEffect(() => {
- form.setFieldsValue({ hyperparam: seniorConfig })
- }, [seniorConfig])
+ setGPU(sys.gpu_count || 0)
+ if (!HIDDENMODULES.OPENPAI) {
+ setOpenpai(!!sys.openpai_enabled)
+ }
+ }, [sys])
useEffect(() => {
- did && func.getDataset(did)
- did && form.setFieldsValue({ datasetId: Number(did) })
- }, [did])
+ setGPU(selectOpenpai ? OPENPAI_MAX_GPU_COUNT : sys.gpu_count || 0)
+ }, [selectOpenpai])
useEffect(() => {
- mid?.length && form.setFieldsValue({ model: mid })
- }, [location.query.mid])
+ pid && getProject({ id: pid, force: true })
+ }, [pid])
useEffect(() => {
- datasetCache[did] && setDataset(datasetCache[did])
- }, [datasetCache])
+ const did = location.query?.did ? getArray(location.query.did).map(Number) : undefined
+
+ did && form.setFieldsValue({ datasets: did })
+ }, [location.query.did])
+
+ useEffect(() => {
+
+ }, [stage])
useEffect(() => {
pid && func.getDatasets(pid)
}, [pid])
useEffect(() => {
- setGpuStep(selectedModels.length || 1)
-
checkModelKeywords()
}, [selectedModels])
+ useEffect(() => {
+ const taskCount = watchStages?.length * watchTestingSets?.length || 1
+ setTaskCount(taskCount)
+ }, [watchStages, watchTestingSets])
+
useEffect(() => {
if (newer.length) {
const tip = <>
@@ -91,16 +115,32 @@ function Inference({ datasetCache, datasets, ...func }) {
}
}, [newer])
- function validHyperparam(rule, value) {
+ useEffect(() => {
+ const state = location.state
+
+ if (state?.record) {
+ const { task: { parameters, config }, description, } = state.record
+ const {
+ dataset_id,
+ docker_image,
+ docker_image_id,
+ model_id,
+ model_stage_id,
+ } = parameters
+ form.setFieldsValue({
+ datasets: [dataset_id],
+ gpu_count: config.gpu_count,
+ stages: [[model_id, model_stage_id]],
+ image: docker_image_id + ',' + docker_image,
+ description,
+ })
+ setSelectedGpu(config.gpu_count)
+ setTimeout(() => setConfig(config), 500)
+ setShowConfig(true)
- const params = form.getFieldValue('hyperparam').map(({ key }) => key)
- .filter(item => item && item.trim() && item === value)
- if (params.length > 1) {
- return Promise.reject(t('task.validator.same.param'))
- } else {
- return Promise.resolve()
+ history.replace({ state: {} })
}
- }
+ }, [location.state])
function checkModelKeywords() {
const keywords = (selectedModels.map(model => model?.keywords) || []).flat().filter(item => item)
@@ -117,17 +157,23 @@ function Inference({ datasetCache, datasets, ...func }) {
function imageChange(_, image = {}) {
const { url, configs = [] } = image
const configObj = configs.find(conf => conf.type === TYPES.INFERENCE) || {}
- setConfig(configObj.config)
+ if (!HIDDENMODULES.LIVECODE) {
+ setLiveCode(image.liveCode || false)
+ }
+ setConfig(removeLiveCodeConfig(configObj.config))
}
- function setConfig(config) {
- const params = Object.keys(config).filter(key => key !== 'gpu_count').map(key => ({ key, value: config[key] }))
- setSeniorConfig(params)
+ function setConfig(config = {}) {
+ setSeniorConfig(config)
}
const onFinish = async (values) => {
- const config = {}
- form.getFieldValue('hyperparam').forEach(({ key, value }) => key && value ? config[key] = value : null)
+ const config = {
+ ...values.hyperparam?.reduce(
+ (prev, { key, value }) => key && value ? { ...prev, [key]: value } : prev,
+ {}),
+ ...(values.live || {}),
+ }
config['gpu_count'] = form.getFieldValue('gpu_count') || 0
@@ -143,13 +189,17 @@ function Inference({ datasetCache, datasets, ...func }) {
image,
config,
}
- const result = await func.createInferenceTask(params)
+ const result = await func.infer(params)
if (result) {
- if (result.filter(item => item).length !== values.model.length) {
+ const tasksCount = values.stages.length * values.datasets.length
+ const resultCount = result.filter(item => item).length
+ if (resultCount < tasksCount) {
message.warn(t('task.inference.failure.some'))
}
await func.clearCache()
- history.replace(`/home/project/detail/${pid}`)
+ const groups = result.map(item => item.result_dataset?.dataset_group_id || '')
+ console.log('groups:', groups, resultCount, taskCount, result)
+ history.replace(`/home/project/${pid}/dataset#${groups.join(',')}`)
}
}
@@ -157,12 +207,9 @@ function Inference({ datasetCache, datasets, ...func }) {
console.log("Failed:", errorInfo)
}
- function setsChange(id) {
- id && setDataset(datasets.find(ds => ds.id === id))
- }
-
function modelChange(id, options = []) {
- setSelectedModels(options.map(({ model }) => model) || [])
+ const models = options.map(([{ model }]) => model) || []
+ setSelectedModels(models)
}
async function selectModelFromIteration() {
@@ -173,9 +220,21 @@ function Inference({ datasetCache, datasets, ...func }) {
}
}
+ const testSetFilters = useCallback(datasets => {
+ const testings = datasets.filter(ds => project.testingSets?.includes(ds.id)).map(ds => ({ ...ds, isProjectTesting: true }))
+ const others = datasets.filter(ds => !project.testingSets?.includes(ds.id))
+ return [...testings, ...others]
+ }, [project.testingSets])
+
+ const renderLabel = item =>
+ {item.name} {item.versionName}(assets: {item.assetCount})
+ {item.isProjectTesting ? t('project.testing.dataset.label') : null}
+
+
const getCheckedValue = (list) => list.find((item) => item.checked)["id"]
const initialValues = {
description: '',
+ stages: stage.length ? stage : undefined,
image: image ? parseInt(image) : undefined,
algorithm: getCheckedValue(Algorithm()),
gpu_count: 0,
@@ -194,162 +253,96 @@ function Inference({ datasetCache, datasets, ...func }) {
initialValues={initialValues}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
- labelAlign={'left'}
- colon={false}
- scrollToFirstError
>
-
history.push(`/home/dataset/add/${pid}`)} />}>
-
-
-
-
-
-
-
-
-
-
}>
-
-
-
-
-
- selectModelFromIteration()}>{t('task.inference.model.iters')}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
- {t('task.infer.gpu.tip', { total: gpu_count, selected: gpuStep * selectedGpu })}
-
+
-
-
- {seniorConfig.length ?
+
+ selectModelFromIteration()}>
+ {t('task.inference.model.iters')}
+
+
+
+
+
+
+
+
+
-
- setHpVisible(!hpVisible)}
- icon={hpVisible ? : }
- style={{ paddingLeft: 0 }}
- >{hpVisible ? t('task.train.fold') : t('task.train.unfold')}
-
-
-
-
- {(fields, { add, remove }) => (
-
- )}
-
-
-
- : null}
-
-
- value <= Math.floor(gpu_count / taskCount) ?
+ Promise.resolve() :
+ Promise.reject(),
+ message: t('task.infer.gpu.tip', { total: gpu_count, selected: taskCount * selectedGpu })
+ }
]}
>
-
-
-
-
-
-
-
-
-
- {t('common.action.infer')}
-
-
-
- history.goBack()}>
- {t('task.btn.back')}
-
-
-
+
-
+
+ {t('task.infer.gpu.tip', { total: gpu_count, selected: taskCount * selectedGpu })}
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('common.action.inference')}
+
+
+
+ history.goBack()}>
+ {t('task.btn.back')}
+
+
+
+
@@ -386,9 +379,9 @@ const dis = (dispatch) => {
clearCache() {
return dispatch({ type: "dataset/clearCache", })
},
- createInferenceTask(payload) {
+ infer(payload) {
return dispatch({
- type: "task/createInferenceTask",
+ type: "task/infer",
payload,
})
},
diff --git a/ymir/web/src/pages/task/label/index.js b/ymir/web/src/pages/task/label/index.js
index 9cf66c9e9f..9982c554d5 100644
--- a/ymir/web/src/pages/task/label/index.js
+++ b/ymir/web/src/pages/task/label/index.js
@@ -1,241 +1,25 @@
-import React, { useEffect, useState } from "react"
-import { connect } from "dva"
-import { Select, Input, Card, Button, Form, Row, Col, Checkbox, ConfigProvider, Space, Radio, Tag, } from "antd"
-import styles from "./index.less"
-import commonStyles from "../common.less"
-import { formLayout } from "@/config/antd"
+import { Card, } from "antd"
+import { useLocation } from "umi"
import t from "@/utils/t"
-import { useHistory, useParams, Link, useLocation } from "umi"
-import Uploader from "@/components/form/uploader"
import Breadcrumbs from "@/components/common/breadcrumb"
-import { randomNumber } from "@/utils/number"
-import Tip from "@/components/form/tip"
-import DatasetSelect from "../../../components/form/datasetSelect"
-const LabelTypes = () => [
- { id: "part", label: t('task.label.form.type.newer'), checked: true },
- { id: "all", label: t('task.label.form.type.all') },
-]
+import commonStyles from "../common.less"
+import Label from "@/components/task/label"
+import useSubmitHandle from "../components/useSubmitHandle"
-function Label({ datasets, keywords, ...func }) {
- const pageParams = useParams()
+function LabelPage() {
const { query } = useLocation()
- const pid = Number(pageParams.id)
- const { iterationId, outputKey, currentStage } = query
- const did = Number(query.did)
- const history = useHistory()
- const [doc, setDoc] = useState(undefined)
- const [form] = Form.useForm()
- const [asChecker, setAsChecker] = useState(false)
-
- useEffect(() => {
- func.getKeywords({ limit: 100000 })
- }, [])
-
- const onFinish = async (values) => {
- const { labellers, checker } = values
- const emails = [labellers]
- checker && emails.push(checker)
- const params = {
- ...values,
- projectId: pid,
- labellers: emails,
- doc,
- name: 'task_label_' + randomNumber(),
- }
- const result = await func.createLabelTask(params)
- if (result) {
- if (iterationId) {
- func.updateIteration({ id: iterationId, currentStage, [outputKey]: result.result_dataset.id })
- }
- await func.clearCache()
- history.replace(`/home/project/detail/${pid}`)
- }
- }
+ const submitHandle = useSubmitHandle()
- function docChange(files, docFile) {
- setDoc(files.length ? location.protocol + '//' + location.host + docFile : '')
- }
-
- function onFinishFailed(errorInfo) {
- console.log("Failed:", errorInfo)
- }
-
- const getCheckedValue = (list) => list.find((item) => item.checked)["id"]
- const initialValues = {
- datasetId: did || undefined,
- keepAnnotations: true,
- labelType: getCheckedValue(LabelTypes()),
- }
return (
)
}
-const dis = (dispatch) => {
- return {
- getDataset(id, force) {
- return dispatch({
- type: "dataset/getDataset",
- payload: { id, force },
- })
- },
- createLabelTask(payload) {
- return dispatch({
- type: "task/createLabelTask",
- payload,
- })
- },
- clearCache() {
- return dispatch({ type: "dataset/clearCache", })
- },
- getKeywords(payload) {
- return dispatch({
- type: 'keyword/getKeywords',
- payload,
- })
- },
- updateIteration(params) {
- return dispatch({
- type: 'iteration/updateIteration',
- payload: params,
- })
- },
- }
-}
-
-const stat = (state) => {
- return {
- datasets: state.dataset.dataset,
- keywords: state.keyword.keywords.items,
- }
-}
-
-export default connect(stat, dis)(Label)
+export default LabelPage
diff --git a/ymir/web/src/pages/task/merge/index.js b/ymir/web/src/pages/task/merge/index.js
new file mode 100644
index 0000000000..e7750b10c2
--- /dev/null
+++ b/ymir/web/src/pages/task/merge/index.js
@@ -0,0 +1,27 @@
+import React, { } from "react"
+import { Form, Card } from "antd"
+import { useLocation } from "umi"
+
+import t from "@/utils/t"
+
+import Breadcrumbs from "@/components/common/breadcrumb"
+
+import commonStyles from "../common.less"
+import Merge from "@/components/task/merge"
+import useSubmitHandle from "../components/useSubmitHandle"
+
+function MergePage() {
+ const { query } = useLocation()
+ const submitHandle = useSubmitHandle()
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default MergePage
diff --git a/ymir/web/src/pages/task/mining/index.js b/ymir/web/src/pages/task/mining/index.js
index 7562ca3d28..7073bb8820 100644
--- a/ymir/web/src/pages/task/mining/index.js
+++ b/ymir/web/src/pages/task/mining/index.js
@@ -1,423 +1,25 @@
-import React, { useEffect, useState } from "react"
-import { connect } from "dva"
-import { Select, Card, Input, Radio, Button, Form, Row, Col, ConfigProvider, Space, InputNumber } from "antd"
-import {
- PlusOutlined,
- MinusCircleOutlined,
- UpSquareOutlined,
- DownSquareOutlined,
-} from '@ant-design/icons'
-import styles from "./index.less"
-import commonStyles from "../common.less"
-import { formLayout } from "@/config/antd"
+import React, { } from "react"
+import { useLocation } from "umi"
+import { Card } from "antd"
import t from "@/utils/t"
-import { TYPES } from '@/constants/image'
-import { useHistory, useParams, useLocation } from "umi"
import Breadcrumbs from "@/components/common/breadcrumb"
-import EmptyStateDataset from '@/components/empty/dataset'
-import EmptyStateModel from '@/components/empty/model'
-import { randomNumber } from "@/utils/number"
-import Tip from "@/components/form/tip"
-import ModelSelect from "@/components/form/modelSelect"
-import ImageSelect from "@/components/form/imageSelect"
-
-const { Option } = Select
-
-const Algorithm = () => [{ id: "aldd", label: 'ALDD', checked: true }]
-const renderRadio = (types) => {
- return (
-
- {types.map((type) => (
-
- {type.label}
-
- ))}
-
- )
-}
-
-function Mining({ datasetCache, datasets, ...func }) {
- const pageParams = useParams()
- const pid = Number(pageParams.id)
- const history = useHistory()
- const location = useLocation()
- const { mid, image, iterationId, currentStage, outputKey } = location.query
- const did = Number(location.query.did)
- const [dataset, setDataset] = useState({})
- const [selectedModel, setSelectedModel] = useState({})
- const [form] = Form.useForm()
- const [seniorConfig, setSeniorConfig] = useState([])
- const [hpVisible, setHpVisible] = useState(false)
- const [topk, setTopk] = useState(true)
- const [gpu_count, setGPU] = useState(0)
- const [imageHasInference, setImageHasInference] = useState(false)
-
- useEffect(() => {
- fetchSysInfo()
- }, [])
-
- useEffect(() => {
- form.setFieldsValue({ hyperparam: seniorConfig })
- }, [seniorConfig])
-
- useEffect(() => {
- did && func.getDataset(did)
- }, [did])
-
- useEffect(() => {
- const cache = datasetCache[did]
- if (cache) {
- setDataset(cache)
- }
- }, [datasetCache])
-
- useEffect(() => {
- pid && func.getDatasets(pid)
- }, [pid])
-
- function validHyperparam(rule, value) {
-
- const params = form.getFieldValue('hyperparam').map(({ key }) => key)
- .filter(item => item && item.trim() && item === value)
- if (params.length > 1) {
- return Promise.reject(t('task.validator.same.param'))
- } else {
- return Promise.resolve()
- }
- }
-
- async function fetchSysInfo() {
- const result = await func.getSysInfo()
- if (result) {
- setGPU(result.gpu_count)
- }
- }
-
- function filterStrategyChange({ target }) {
- setTopk(target.value)
- }
-
- function imageChange(_, image = {}) {
- const { url, configs = [] } = image
- const configObj = configs.find(conf => conf.type === TYPES.MINING) || {}
- const hasInference = configs.some(conf => conf.type === TYPES.INFERENCE)
- setImageHasInference(hasInference)
- !hasInference && form.setFieldsValue({ inference: false })
- setConfig(configObj.config)
- }
-
- function setConfig(config) {
- const params = Object.keys(config).filter(key => key !== 'gpu_count').map(key => ({ key, value: config[key] }))
- setSeniorConfig(params)
- }
-
- const onFinish = async (values) => {
- const config = {}
- form.getFieldValue('hyperparam').forEach(({ key, value }) => key && value ? config[key] = value : null)
-
- config['gpu_count'] = form.getFieldValue('gpu_count') || 0
+import Mining from "@/components/task/mining"
- const img = (form.getFieldValue('image') || '').split(',')
- const imageId = Number(img[0])
- const image = img[1]
- const params = {
- ...values,
- name: 'task_mining_' + randomNumber(),
- topk: values.filter_strategy ? values.topk : 0,
- projectId: pid,
- imageId,
- image,
- config,
- }
- const result = await func.createMiningTask(params)
- if (result) {
- if (iterationId) {
- func.updateIteration({ id: iterationId, currentStage, [outputKey]: result.result_dataset.id })
- }
- await func.clearCache()
- history.replace(`/home/project/detail/${pid}`)
- }
- }
-
- function onFinishFailed(errorInfo) {
- console.log("Failed:", errorInfo)
- }
-
- function setsChange(id) {
- id && setDataset(datasets.find(ds => ds.id === id))
- }
-
- function modelChange(id, { model }) {
- model && setSelectedModel(model)
- }
+import commonStyles from "../common.less"
+import useSubmitHandle from "../components/useSubmitHandle"
- const getCheckedValue = (list) => list.find((item) => item.checked)["id"]
- const initialValues = {
- model: mid ? parseInt(mid) : undefined,
- image: image ? parseInt(image) : undefined,
- datasetId: did ? did : undefined,
- algorithm: getCheckedValue(Algorithm()),
- topk: 0,
- gpu_count: 0,
- }
+function MiningPage() {
+ const { query } = useLocation()
+ const submitHandle = useSubmitHandle()
return (
-
-
- history.push(`/home/dataset/add/${pid}`)} />}>
-
-
-
- option.children.join('').toLowerCase().indexOf(input.toLowerCase()) >= 0}
- onChange={setsChange}
- showArrow
- >
- {datasets.map(item =>
-
- {item.name} {item.versionName}(assets: {item.assetCount})
- )}
-
-
-
-
-
-
- }>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {renderRadio(Algorithm())}
-
-
-
-
-
-
-
- {t('common.all')}
- {t('task.mining.form.topk.label')}
-
-
-
-
-
- {t('task.mining.topk.tip')}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('task.gpu.tip', { count: gpu_count })}
-
-
-
- {seniorConfig.length ?
-
-
- setHpVisible(!hpVisible)}
- icon={hpVisible ? : }
- style={{ paddingLeft: 0 }}
- >{hpVisible ? t('task.train.fold') : t('task.train.unfold')}
-
-
-
-
- {(fields, { add, remove }) => (
-
- )}
-
-
-
- : null}
-
-
-
-
-
- {t('common.action.mine')}
-
-
-
- history.goBack()}>
- {t('task.btn.back')}
-
-
-
-
-
-
-
+
)
}
-const props = (state) => {
- return {
- datasets: state.dataset.allDatasets,
- datasetCache: state.dataset.dataset,
- }
-}
-
-const dis = (dispatch) => {
- return {
- getSysInfo() {
- return dispatch({
- type: "common/getSysInfo",
- })
- },
- getDatasets(pid, force = true) {
- return dispatch({
- type: "dataset/queryAllDatasets",
- payload: { pid, force },
- })
- },
- getDataset(id, force) {
- return dispatch({
- type: "dataset/getDataset",
- payload: { id, force },
- })
- },
- clearCache() {
- return dispatch({ type: "dataset/clearCache", })
- },
- createMiningTask(payload) {
- return dispatch({
- type: "task/createMiningTask",
- payload,
- })
- },
- updateIteration(params) {
- return dispatch({
- type: 'iteration/updateIteration',
- payload: params,
- })
- },
- }
-}
-
-export default connect(props, dis)(Mining)
+export default MiningPage
diff --git a/ymir/web/src/pages/task/train/index.js b/ymir/web/src/pages/task/train/index.js
index 47411e94d0..b06f821472 100644
--- a/ymir/web/src/pages/task/train/index.js
+++ b/ymir/web/src/pages/task/train/index.js
@@ -1,492 +1,26 @@
-import React, { useEffect, useState } from "react"
-import { connect } from "dva"
-import { Select, Card, Input, Radio, Button, Form, Row, Col, ConfigProvider, Space, InputNumber, Tag, message } from "antd"
-import {
- PlusOutlined,
- MinusCircleOutlined,
- UpSquareOutlined,
- DownSquareOutlined,
-} from '@ant-design/icons'
-import { formLayout } from "@/config/antd"
-import { useHistory, useParams, useLocation } from "umi"
+import { Card } from "antd"
+import { useLocation } from "umi"
import t from "@/utils/t"
-import { TYPES } from '@/constants/image'
-import Breadcrumbs from "@/components/common/breadcrumb"
-import EmptyState from '@/components/empty/dataset'
-import EmptyStateModel from '@/components/empty/model'
-import { randomNumber } from "@/utils/number"
-import Tip from "@/components/form/tip"
-import ImageSelect from "@/components/form/imageSelect"
-import styles from "./index.less"
-import commonStyles from "../common.less"
-import ModelSelect from "@/components/form/modelSelect"
-import KeywordRates from "@/components/dataset/keywordRates"
-import CheckProjectDirty from "@/components/common/CheckProjectDirty"
-
-const { Option } = Select
-
-const TrainType = () => [{ id: "detection", label: t('task.train.form.traintypes.detect'), checked: true }]
-const FrameworkType = () => [{ id: "YOLO v4", label: "YOLO v4", checked: true }]
-const Backbone = () => [{ id: "darknet", label: "Darknet", checked: true }]
-
-function Train({ allDatasets, datasetCache, keywords, ...func }) {
- const pageParams = useParams()
- const pid = Number(pageParams.id)
- const history = useHistory()
- const location = useLocation()
- const { mid, image, iterationId, outputKey, currentStage, test } = location.query
- const did = Number(location.query.did)
- const [project, setProject] = useState({})
- const [datasets, setDatasets] = useState([])
- const [dataset, setDataset] = useState({})
- const [trainSet, setTrainSet] = useState(null)
- const [testSet, setTestSet] = useState(null)
- const [form] = Form.useForm()
- const [seniorConfig, setSeniorConfig] = useState([])
- const [hpVisible, setHpVisible] = useState(false)
- const [gpu_count, setGPU] = useState(0)
- const [projectDirty, setProjectDirty] = useState(false)
-
- const renderRadio = (types) => {
- return (
-
- {types.map((type) => (
-
- {type.label}
-
- ))}
-
- )
- }
-
- useEffect(() => {
- fetchSysInfo()
- fetchProject()
- }, [])
-
- useEffect(() => {
- func.getKeywords({ limit: 100000 })
- }, [])
-
- useEffect(() => {
- const dss = allDatasets.filter(ds => ds.keywords.some(kw => project?.keywords?.includes(kw)))
- setDatasets(dss)
- const isValid = dss.some(ds => ds.id === did)
- const visibleValue = isValid ? did : null
- setTrainSet(visibleValue)
- form.setFieldsValue({ datasetId: visibleValue })
- }, [allDatasets, project])
-
- useEffect(() => {
- did && func.getDataset(did)
- }, [did])
-
- useEffect(() => {
- const dst = datasetCache[did]
- dst && setDataset(dst)
- }, [datasetCache])
-
- useEffect(() => {
- pid && func.getDatasets(pid)
- }, [pid])
-
- useEffect(() => {
- form.setFieldsValue({ hyperparam: seniorConfig })
- }, [seniorConfig])
-
- async function validHyperparam(rule, value) {
-
- const params = form.getFieldValue('hyperparam').map(({ key }) => key)
- .filter(item => item && item.trim() && item === value)
- if (params.length > 1) {
- return Promise.reject(t('task.validator.same.param'))
- } else {
- return Promise.resolve()
- }
- }
-
- async function fetchSysInfo() {
- const result = await func.getSysInfo()
- if (result) {
- setGPU(result.gpu_count)
- }
- }
-
- async function fetchProject() {
- const project = await func.getProject(pid)
- project && setProject(project)
- form.setFieldsValue({ keywords: project.keywords })
- }
-
- function trainSetChange(value) {
- setTrainSet(value)
- }
- function validationSetChange(value) {
- setTestSet(value)
- }
- function modelChange(value, model) {
- setSelectedModel(model)
- }
-
- function imageChange(_, image = {}) {
- const { configs } = image
- const configObj = (configs || []).find(conf => conf.type === TYPES.TRAINING) || {}
- setConfig(configObj.config)
- }
-
- function setConfig(config = {}) {
- const params = Object.keys(config).filter(key => key !== 'gpu_count').map(key => ({ key, value: config[key] }))
- setSeniorConfig(params)
- }
-
- const onFinish = async (values) => {
- const config = {}
- form.getFieldValue('hyperparam').forEach(({ key, value }) => key && value ? config[key] = value : null)
+import Breadcrumbs from "@/components/common/breadcrumb"
+import Training from "@/components/task/training"
- const gpuCount = form.getFieldValue('gpu_count')
- // if (gpuCount) {
- config['gpu_count'] = gpuCount || 0
- // }
- const img = (form.getFieldValue('image') || '').split(',')
- const imageId = Number(img[0])
- const image = img[1]
- const params = {
- ...values,
- name: 'group_' + randomNumber(),
- projectId: pid,
- keywords: iterationId ? project.keywords : values.keywords,
- image,
- imageId,
- config,
- }
- const result = await func.createTrainTask(params)
- if (result) {
- if (iterationId) {
- func.updateIteration({ id: iterationId, currentStage, [outputKey]: result.result_model.id })
- }
- await func.clearCache()
- history.replace(`/home/project/detail/${pid}#model`)
- }
- }
+import commonStyles from "../common.less"
+import useSubmitHandle from "../components/useSubmitHandle"
- function onFinishFailed(errorInfo) {
- console.log("Failed:", errorInfo)
- }
+function TrainPage() {
+ const { query } = useLocation()
+ const submitHandle = useSubmitHandle('model')
- const getCheckedValue = (list) => list.find((item) => item.checked)["id"]
- const initialValues = {
- name: 'task_train_' + randomNumber(),
- datasetId: did ? did : undefined,
- testset: Number(test) ? Number(test) : undefined,
- image: image ? parseInt(image) : undefined,
- model: mid ? parseInt(mid) : undefined,
- trainType: getCheckedValue(TrainType()),
- network: getCheckedValue(FrameworkType()),
- backbone: getCheckedValue(Backbone()),
- gpu_count: 1,
- }
return (
-
-
setProjectDirty(dirty)} />
-
- history.push(`/home/dataset/add/${pid}`)} />}>
-
-
- option.children.join('').toLowerCase().indexOf(input.toLowerCase()) >= 0}
- onChange={trainSetChange}
- showArrow
- >
- {datasets.filter(ds => ds.id !== testSet).map(item =>
-
- {item.name} {item.versionName}(assets: {item.assetCount})
-
- )}
-
-
-
-
-
- option.children.join('').toLowerCase().indexOf(input.toLowerCase()) >= 0}
- onChange={validationSetChange}
- showArrow
- >
- {datasets.filter(ds => ds.id !== trainSet).map(item =>
-
- {item.name} {item.versionName}({item.assetCount})
-
- )}
-
-
-
-
-
-
-
-
-
-
- {iterationId ?
- {project?.keywords?.map(keyword => {keyword} )}
- :
-
- [option.value, ...(option.aliases || [])].some(key => key.indexOf(value) >= 0)}>
- {keywords.map(keyword => (
-
-
- {keyword.name}
-
-
- ))}
-
- }
-
- }>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {renderRadio(TrainType())}
-
-
-
-
-
- {renderRadio(FrameworkType())}
-
-
-
-
-
- {renderRadio(Backbone())}
-
-
-
-
-
-
-
- {t('task.gpu.tip', { count: gpu_count })}
-
-
-
- {seniorConfig.length ?
-
-
- setHpVisible(!hpVisible)}
- icon={hpVisible ? : }
- style={{ paddingLeft: 0 }}
- >{hpVisible ? t('task.train.fold') : t('task.train.unfold')}
-
-
-
-
- {(fields, { add, remove }) => (
- <>
-
- >
- )}
-
-
-
- : null}
-
-
-
-
-
- {t('common.action.train')}
-
-
-
- history.goBack()}>
- {t('task.btn.back')}
-
-
-
-
-
-
-
+
)
}
-const props = (state) => {
- return {
- allDatasets: state.dataset.allDatasets,
- datasetCache: state.dataset.dataset,
- keywords: state.keyword.keywords.items,
- }
-}
-
-const dis = (dispatch) => {
- return {
- getProject(id) {
- return dispatch({
- type: "project/getProject",
- payload: { id },
- })
- },
- getDatasets(pid, force = true) {
- return dispatch({
- type: "dataset/queryAllDatasets",
- payload: { pid, force },
- })
- },
- getDataset(id, force) {
- return dispatch({
- type: "dataset/getDataset",
- payload: { id, force },
- })
- },
- clearCache() {
- return dispatch({ type: "model/clearCache", })
- },
- getSysInfo() {
- return dispatch({
- type: "common/getSysInfo",
- })
- },
- createTrainTask(payload) {
- return dispatch({
- type: "task/createTrainTask",
- payload,
- })
- },
- updateIteration(params) {
- return dispatch({
- type: 'iteration/updateIteration',
- payload: params,
- })
- },
- getKeywords() {
- return dispatch({
- type: 'keyword/getKeywords',
- payload: { limit: 10000 },
- })
- },
- }
-}
-
-export default connect(props, dis)(Train)
+export default TrainPage
diff --git a/ymir/web/src/pages/user/signup/index.js b/ymir/web/src/pages/user/signup/index.js
index b14f402bd3..2c6aa8792f 100644
--- a/ymir/web/src/pages/user/signup/index.js
+++ b/ymir/web/src/pages/user/signup/index.js
@@ -9,7 +9,7 @@ import { layout420 } from "@/config/antd"
import HeaderNav from "@/components/nav"
import Foot from "@/components/common/footer"
import styles from "../common.less"
-import { EmailIcon, UserIcon, SmartphoneIcon, LockIcon, KeyIcon } from '@/components/common/icons'
+import { EmailIcon, UserIcon, SmartphoneIcon, LockIcon, KeyIcon, EqualizerIcon, NavHomeIcon } from '@/components/common/icons'
import { phoneValidate } from "@/components/form/validators"
const { Header, Footer, Content } = Layout
@@ -24,11 +24,13 @@ const Signup = ({ signupApi, loginApi, history }) => {
return Promise.resolve()
},
})
- const signup = async ({ email, username, phone = '', password }) => {
+ const signup = async ({ email, username, phone = '', password, scene = '', organization = '' }) => {
const params = {
email,
username: username.trim(),
phone: phone.trim(),
+ organization: organization.trim(),
+ scene: scene.trim(),
password,
}
const res = await signupApi(params)
@@ -113,6 +115,18 @@ const Signup = ({ signupApi, loginApi, history }) => {
>
} />
+
+ } />
+
+
+ } />
+
{
})
it("evaluate -> normal return", () => {
const datasets = [2342353, 2345]
- const gt = 234234
- const params = { projectId: 25343, datasets, gt, confidence: 0.6 }
+ const iou = 0.65
+ const params = { projectId: 25343, datasets, iou, everageIou: false, confidence: 0.6 }
const expected = datasets.reduce((prev, ds) => ({
...prev,
- [ds]: { prev_dataset_id: gt }
+ [ds]: { [iou]: iou }
}), {})
requestExample(evaluate, params, expected, 'post', 111902)
})
diff --git a/ymir/web/src/services/__test__/model.test.js b/ymir/web/src/services/__test__/model.test.js
index 6e847fb8fe..102f93e2e2 100644
--- a/ymir/web/src/services/__test__/model.test.js
+++ b/ymir/web/src/services/__test__/model.test.js
@@ -7,6 +7,7 @@ import {
importModel,
updateModel,
verify,
+ setRecommendStage,
} from "../model"
import { product, products, requestExample } from './func'
@@ -49,6 +50,12 @@ describe("service: models", () => {
const expected = { id, name }
requestExample(updateModel, [id, name], expected)
})
+ it("setRecommendStage -> success", () => {
+ const model = 63437
+ const stage = 24234
+ const expected = { id: model, recommended_stage: stage }
+ requestExample(setRecommendStage, [model, stage], expected)
+ })
it("importModel -> success", () => {
const params = {
name: 'newmodel',
@@ -57,7 +64,7 @@ describe("service: models", () => {
requestExample(importModel, params, expected, 'post')
})
it("veirfy -> success", () => {
- const params = { model_id: 754, image_urls: ['/path/to/image'], image: 'dockerimage:latest' }
+ const params = { modelStage: [524, 754], urls: ['/path/to/image'], image: 'dockerimage:latest' }
const expected = "ok"
requestExample(verify, params, expected, 'post')
})
diff --git a/ymir/web/src/services/__test__/project.test.js b/ymir/web/src/services/__test__/project.test.js
index 5ed9a43f00..5c68c9e2c8 100644
--- a/ymir/web/src/services/__test__/project.test.js
+++ b/ymir/web/src/services/__test__/project.test.js
@@ -41,7 +41,7 @@ describe("service: projects", () => {
training_dataset_count_target: 0,
type: 0,
}
- const expected = { id, name }
+ const expected = { id }
requestExample(updateProject, [id, project], expected, 'post')
})
it("createProject -> success", () => {
diff --git a/ymir/web/src/services/__test__/task.test.js b/ymir/web/src/services/__test__/task.test.js
index ac064b8f4d..88bb23c33a 100644
--- a/ymir/web/src/services/__test__/task.test.js
+++ b/ymir/web/src/services/__test__/task.test.js
@@ -3,10 +3,12 @@ import {
getTask,
deleteTask,
updateTask,
- createFusionTask,
- createLabelTask,
- createTrainTask,
- createMiningTask,
+ fusion,
+ filter,
+ merge,
+ label,
+ train,
+ mine,
createTask,
stopTask,
} from "../task"
@@ -29,16 +31,16 @@ describe("service: tasks", () => {
requestExample(getTask, id, expected, 'get')
})
- it("createFusionTask -> success, no include classes", () => {
+ it("fusion -> success, no include classes", () => {
const params = {
project_id: 2436,
dataset_group_id: 2345,
main_dataset_id: 435,
}
const expected = { id: 612 }
- requestExample(createFusionTask, params, expected, 'post')
+ requestExample(fusion, params, expected, 'post')
})
- it("createFusionTask -> success, no exclude classes", () => {
+ it("fusion -> success, no exclude classes", () => {
const params = {
project_id: 2436,
dataset_group_id: 2340,
@@ -46,9 +48,9 @@ describe("service: tasks", () => {
include_datasets: [454, 457],
}
const expected = { id: 612 }
- requestExample(createFusionTask, params, expected, 'post')
+ requestExample(fusion, params, expected, 'post')
})
- it("createFusionTask -> success, all params", () => {
+ it("fusion -> success, all params", () => {
const params = {
project_id: 2436,
dataset_group_id: 2340,
@@ -61,10 +63,63 @@ describe("service: tasks", () => {
sampling_count: 1000,
}
const expected = { id: 612 }
- requestExample(createFusionTask, params, expected, 'post')
+ requestExample(fusion, params, expected, 'post')
+ })
+ it("merge -> success, no include classes", () => {
+ const params = {
+ project_id: 2436,
+ dataset_group_id: 2345,
+ main_dataset_id: 435,
+ }
+ const expected = { id: 612 }
+ requestExample(merge, params, expected, 'post')
+ })
+ it("merge -> success, no exclude classes", () => {
+ const params = {
+ project_id: 2436,
+ dataset_group_id: 2340,
+ main_dataset_id: 432,
+ include_datasets: [454, 457],
+ }
+ const expected = { id: 612 }
+ requestExample(merge, params, expected, 'post')
+ })
+ it("merge -> success, all params", () => {
+ const params = {
+ project_id: 2436,
+ dataset_group_id: 2340,
+ main_dataset_id: 432,
+ include_datasets: [454, 457],
+ include_strategy: 2,
+ exclude_datasets: [4,5],
+ }
+ const expected = { id: 612 }
+ requestExample(merge, params, expected, 'post')
+ })
+
+ it("filter -> success, no include classes", () => {
+ const params = {
+ project_id: 2436,
+ main_dataset_id: 435,
+ include_labels: ['person'],
+ }
+ const expected = { id: 612 }
+ requestExample(filter, params, expected, 'post')
+ })
+ it("filter -> success, all params", () => {
+ const params = {
+ project_id: 2436,
+ dataset_group_id: 2340,
+ main_dataset_id: 432,
+ include_labels: ['person'],
+ exclude_labels: ['cat', 'dog', 'tree'],
+ sampling_count: 1000,
+ }
+ const expected = { id: 612 }
+ requestExample(filter, params, expected, 'post')
})
- it("createTrainTask -> success", () => {
+ it("train -> success", () => {
const params = {
name: 'taskname',
train_sets: [1, 3, 4],
@@ -76,9 +131,9 @@ describe("service: tasks", () => {
train_type: 1,
}
const expected = { id: 611 }
- requestExample(createTrainTask, params, expected, 'post')
+ requestExample(train, params, expected, 'post')
})
- it("createMiningTask -> success", () => {
+ it("mine -> success", () => {
const params = {
model: 'modelhash',
topk: 1000,
@@ -88,9 +143,9 @@ describe("service: tasks", () => {
name: 'taskname',
}
const expected = { id: 610 }
- requestExample(createMiningTask, params, expected, 'post')
+ requestExample(mine, params, expected, 'post')
})
- it("createLabelTask -> success", () => {
+ it("label -> success", () => {
const params = {
name: 'taskname',
datasets: [23, 34],
@@ -99,7 +154,7 @@ describe("service: tasks", () => {
doc: 'http://test.com/test.pdf'
}
const expected = { id: 609 }
- requestExample(createLabelTask, params, expected, 'post')
+ requestExample(label, params, expected, 'post')
})
it("deleteTask -> success", () => {
const id = 608
diff --git a/ymir/web/src/services/common.js b/ymir/web/src/services/common.js
index 33e625cf75..2a171db494 100644
--- a/ymir/web/src/services/common.js
+++ b/ymir/web/src/services/common.js
@@ -15,9 +15,17 @@ export function getHistory({ type, id, max_hops }) {
return request.get('/graphs/', { params: { type, id, max_hops } })
}
-export function getTensorboardLink(hash) {
- hash = hash ? hash : ''
- return `/tensorboard/#scalars®exInput=${hash}`
+/**
+ * generate tensorboard link
+ * @param {array|string} hashs
+ * @returns
+ */
+export function getTensorboardLink(hashs = []) {
+ if (!Array.isArray(hashs)) {
+ hashs = [hashs]
+ }
+ const query = hashs.filter(hash => hash).join('|')
+ return `/tensorboard/#scalars®exInput=${query}`
}
/**
diff --git a/ymir/web/src/services/dataset.js b/ymir/web/src/services/dataset.js
index d1e528c48d..948d279b26 100644
--- a/ymir/web/src/services/dataset.js
+++ b/ymir/web/src/services/dataset.js
@@ -6,8 +6,8 @@ import request from "@/utils/request"
* @param {array[number]} id
* @returns
*/
-export function getDataset(id) {
- return request.get(`datasets/${id}`)
+export function getDataset(id, verbose) {
+ return request.get(`datasets/${id}`, { params: { verbose } })
}
/**
@@ -65,8 +65,21 @@ export function getDatasetGroups(project_id, { name, offset = 0, limit = 10 }) {
return request.get("dataset_groups/", { params: { project_id, name, offset, limit } })
}
-export function batchDatasets(ids) {
- return request.get('datasets/batch', { params: { ids: ids.toString() } })
+/**
+ * batch getting dataset
+ * @param {array} ids dataset ids
+ * @param {number} pid project id
+ * @param {boolean} ck need ck
+ * @returns
+ */
+export function batchDatasets(pid, ids = [], ck) {
+ return request.get('datasets/batch', {
+ params: {
+ project_id: pid,
+ ids: ids.toString(),
+ ck,
+ }
+ })
}
/**
@@ -76,13 +89,18 @@ export function batchDatasets(ids) {
*/
export function getAssetsOfDataset({
id,
- keyword = null,
+ type = 'keywords',
+ keywords = [],
+ cm = [],
+ annoType = [],
offset = 0,
limit = 20,
}) {
return request.get(`datasets/${id}/assets`, {
params: {
- keyword,
+ [type]: keywords.toString() || undefined,
+ cm_types: cm.toString() || undefined,
+ annotation_types: annoType.toString() || undefined,
offset,
limit,
},
@@ -121,16 +139,34 @@ export function delDatasetGroup(id) {
* evalute between gt and target dataset
* @param {number} projectId project id
* @param {number} datasets evaluational datasets
- * @param {number} gt ground truth dataset
+ * @param {number} iou iou threadhold
+ * @param {number} everageIou
* @param {number} confidence range: [0, 1]
+ * @param {string} ck custom keyword
* @returns
*/
-export function evaluate({ projectId, datasets, gt, confidence }) {
+export function evaluate({ projectId, datasets, iou, everageIou, confidence, ck }) {
return request.post(`/datasets/evaluation`, {
project_id: projectId,
- other_dataset_ids: datasets,
- gt_dataset_id: gt,
+ dataset_ids: datasets,
confidence_threshold: confidence,
+ iou_threshold: iou,
+ require_average_iou: everageIou,
+ main_ck: ck,
+ })
+}
+
+/**
+ * @param {array} datasets analysis datasets
+ * @returns
+ */
+export function analysis(projectId, datasets) {
+ return request.get(`/datasets/batch`, {
+ params: {
+ project_id: projectId,
+ ids: datasets.toString(),
+ hist: true,
+ }
})
}
@@ -161,7 +197,7 @@ export function batchAct(action, projectId, ids = []) {
* }
* @returns
*/
-export function createDataset({ name, projectId, url, datasetId, path, strategy, description }) {
+export function createDataset({ name, projectId, url, datasetId, path, strategy = 2, description }) {
return request.post("/datasets/importing", {
group_name: name, strategy,
project_id: projectId,
@@ -185,3 +221,30 @@ export function updateDataset(id, name) {
export function getInternalDataset() {
return request.get('/datasets/public')
}
+
+/**
+ * check train set and validation set duplication
+ * @param {number} projectId
+ * @param {number} trainSet
+ * @param {number} validationSet
+ * @returns
+ */
+export function checkDuplication(projectId, trainSet, validationSet) {
+ return request.post('/datasets/check_duplication', {
+ project_id: projectId,
+ dataset_ids: [trainSet, validationSet],
+ })
+}
+
+export function getNegativeKeywords({
+ projectId,
+ dataset,
+ keywords,
+}) {
+ return request.get(`/datasets/${dataset}`, {
+ params: {
+ project_id: projectId,
+ keywords: keywords.toString(),
+ }
+ })
+}
diff --git a/ymir/web/src/services/iteration.js b/ymir/web/src/services/iteration.js
index 350ce78958..99055c3211 100644
--- a/ymir/web/src/services/iteration.js
+++ b/ymir/web/src/services/iteration.js
@@ -8,7 +8,7 @@ import request from "@/utils/request"
* @returns
*/
export function getIteration(project_id, id) {
- return request.get(`iterations/${id}`, { params: { project_id }})
+ return request.get(`iterations/${id}`, { params: { project_id } })
}
/**
@@ -40,6 +40,7 @@ export function createIteration({
prevIteration,
projectId,
testSet,
+ miningSet,
}) {
return request.post("/iterations/", {
name,
@@ -47,7 +48,8 @@ export function createIteration({
iteration_round: iterationRound,
project_id: projectId,
previous_iteration: prevIteration,
- testing_dataset_id: testSet,
+ validation_dataset_id: testSet,
+ mining_dataset_id: miningSet,
})
}
/**
@@ -87,3 +89,13 @@ export function updateIteration(
},
})
}
+
+/**
+ * get mining dataset stats for iterations
+ * @param {number} projectId
+ * @param {number} iterationId
+ * @returns
+ */
+export function getMiningStats(projectId, iterationId) {
+ return request.get(`/iterations/${iterationId}/mining_progress?project_id=${projectId}`)
+}
diff --git a/ymir/web/src/services/keyword.js b/ymir/web/src/services/keyword.js
index f882b64083..ddd52d735d 100644
--- a/ymir/web/src/services/keyword.js
+++ b/ymir/web/src/services/keyword.js
@@ -24,6 +24,12 @@ export function updateKeywords({ keywords = [], dry_run = false }) {
})
}
+export function checkDuplication(keywords = []) {
+ return request.post('/keywords/check_duplication', {
+ keywords: keywords.map(keyword => ({ name: keyword })),
+ })
+}
+
/**
* update keyword
* @param {string} name
diff --git a/ymir/web/src/services/model.js b/ymir/web/src/services/model.js
index eac68e11e0..90145a7708 100644
--- a/ymir/web/src/services/model.js
+++ b/ymir/web/src/services/model.js
@@ -144,11 +144,40 @@ export function updateModel(id, name) {
/**
* model verification
- * @param {number} model_id model id
- * @param {array} image_urls image urls
+ * @param {number} projectId project id
+ * @param {array} modelStage model stage
+ * @param {array} urls image urls
* @param {number} image docker image url
+ * @param {object} config docker image configure
* @returns
*/
-export function verify(model_id, image_urls, image, config) {
- return request.post(`/inferences/`, { model_id, image_urls, docker_image: image, docker_image_config: config })
+export function verify({ projectId, modelStage, urls, image, config }) {
+ const [model, stage] = modelStage
+ return request.post(`/inferences/`, {
+ project_id: projectId,
+ model_id: model,
+ model_stage_id: stage,
+ image_urls: urls,
+ docker_image: image,
+ docker_image_config: config
+ })
+}
+
+export function setRecommendStage(model, stage) {
+ return request({
+ method: 'PATCH',
+ url: `/models/${model}`,
+ data: {
+ stage_id: stage,
+ }
+ })
+}
+
+/**
+ * batch fetch model stages
+ * @param {array} ids
+ * @returns
+ */
+export function batchModelStages(ids) {
+ return request.get('model_stages/batch', { params: { ids: ids.toString() } })
}
diff --git a/ymir/web/src/services/project.js b/ymir/web/src/services/project.js
index c8e3d6cbef..eac7f873c1 100644
--- a/ymir/web/src/services/project.js
+++ b/ymir/web/src/services/project.js
@@ -41,6 +41,7 @@ export function delProject(id) {
* {number} [target_map]
* {number} [target_dataset]
* {array} keywords
+ * {boolean} enableIteration
* }
* @returns
*/
@@ -48,12 +49,16 @@ export function createProject({
name,
description,
keywords,
+ strategy = 1,
+ enableIteration,
}) {
return request.post("/projects/", {
name,
description,
training_type: 1,
training_keywords: keywords,
+ mining_strategy: strategy,
+ enable_iteration: enableIteration
})
}
@@ -77,7 +82,9 @@ export function addExampleProject() {
* {number} trainSetVersion
* {number} miningSet
* {number} testSet
- * {number} model
+ * {number} [modelStage]
+ * {boolean} enableIteration
+ * {array} [testingSets]
* }
* @returns
*/
@@ -87,11 +94,15 @@ export function updateProject(id, {
strategy,
chunkSize,
description,
+ candidateTrainSet,
trainSetVersion,
miningSet,
testSet,
- model,
+ modelStage = [],
+ enableIteration,
+ testingSets,
}) {
+ const [model, stage] = modelStage
return request({
method: "patch",
url: `/projects/${id}`,
@@ -101,10 +112,14 @@ export function updateProject(id, {
mining_strategy: strategy,
chunk_size: chunkSize,
mining_dataset_id: miningSet,
- testing_dataset_id: testSet,
+ validation_dataset_id: testSet,
description,
initial_model_id: model,
+ initial_model_stage_id: stage,
initial_training_dataset_id: trainSetVersion,
+ candidate_training_dataset_id: candidateTrainSet,
+ enable_iteration: enableIteration,
+ testing_dataset_ids: testingSets ? testingSets?.toString() : undefined,
},
})
}
diff --git a/ymir/web/src/services/task.js b/ymir/web/src/services/task.js
index 765bf70b81..69d78f8cbc 100644
--- a/ymir/web/src/services/task.js
+++ b/ymir/web/src/services/task.js
@@ -12,10 +12,14 @@ import { TASKTYPES } from "@/constants/task"
* end_time {timestamp} end time of create time (for filter)
* offset {number} start offset
* limit {number} items of fetch, as page size
+ * stages {array} main stages for task
+ * datasets {array} main dataset for task
* }
* @returns {Promise}
*/
export function getTasks({
+ stages = [],
+ datasets = [],
name,
type,
state,
@@ -26,6 +30,8 @@ export function getTasks({
is_desc,
order_by,
}) {
+ const stageIds = stages.toString() || null
+ const datasetIds = datasets.toString() || null
return request.get("/tasks/", {
params: {
name,
@@ -37,6 +43,8 @@ export function getTasks({
limit,
is_desc,
order_by,
+ model_stage_ids: stageIds,
+ dataset_ids: datasetIds,
},
})
}
@@ -102,11 +110,13 @@ export function updateTask(id, name) {
* }
* @returns
*/
-export function createFusionTask({
+export function fusion({
iteration, project_id, group_id, dataset, include_datasets = [], mining_strategy, include_strategy = 2,
exclude_result, exclude_datasets = [], include = [], exclude = [], samples,
+ result_description: description,
}) {
return request.post('/datasets/fusion', {
+ result_description: description,
project_id, include_datasets, exclude_datasets,
iteration_context: iteration ? {
iteration_id: iteration,
@@ -122,6 +132,63 @@ export function createFusionTask({
})
}
+/**
+ * create merge task
+ * @param {object} param0
+ * {
+ * {number} projectId
+ * {number} [group] group id for generated to
+ * {string} [name] dataset name for generated
+ * {array} [datasets] merge datasets
+ * {array} [excludes]
+ * {number} [strategy]
+ * {number} description
+ * }
+ * @returns
+ */
+export function merge({
+ projectId, group, name,
+ datasets = [], strategy = 2,
+ excludes = [],
+ description,
+}) {
+ return request.post('/datasets/merge', {
+ project_id: projectId,
+ include_datasets: datasets,
+ exclude_datasets: excludes,
+ dest_group_name: name,
+ dest_group_id: group,
+ merge_strategy: strategy,
+ description,
+ })
+}
+/**
+ * create filter task
+ * @param {object} param0
+ * {
+ * {number} projectId
+ * {number} dataset
+ * {array} [includes]
+ * {array} [excludes]
+ * {number} [samples]
+ * {number} [description]
+ * }
+ * @returns
+ */
+export function filter({
+ projectId, dataset,
+ includes, excludes, samples,
+ description,
+}) {
+ return request.post('/datasets/filter', {
+ project_id: projectId,
+ dataset_id: dataset,
+ include_keywords: includes,
+ exclude_keywords: excludes,
+ sampling_count: samples,
+ description,
+ })
+}
/**
* create label task
* @param {object} task {
@@ -134,13 +201,14 @@ export function createFusionTask({
* }
* @returns
*/
-export function createLabelTask({
+export function label({
projectId, iteration, stage,
groupId, name, datasetId, keywords,
- labellers, keepAnnotations, doc,
+ labellers, keepAnnotations, doc, description,
}) {
return createTask({
name,
+ result_description: description,
type: TASKTYPES.LABEL,
project_id: projectId,
iteration_id: iteration,
@@ -149,9 +217,9 @@ export function createLabelTask({
dataset_group_id: groupId,
dataset_id: datasetId,
keywords,
- labellers,
+ labellers: ['hide@label.com'],
extra_url: doc,
- keep_annotations: keepAnnotations,
+ annotation_type: keepAnnotations,
},
})
}
@@ -162,30 +230,35 @@ export function createLabelTask({
* {string} name
* {number} projectId
* {number} datasetId
+ * {number} stage
* {number} testset
* {string} backbone
* {object} config
* {string} network
* {number} trainType
* {number} strategy
- * {number} model
+ * {array[number, number]} modelStage
* {string} image
* }
* @returns
*/
-export function createTrainTask({
- iteration, stage,
+export function train({
+ iteration, stage, openpai, description,
name, projectId, datasetId, keywords, testset,
backbone, config, network, trainType, strategy,
- model, image, imageId,
+ modelStage = [], image, imageId, preprocess,
}) {
+ const model = modelStage[0]
+ const stageId = modelStage[1]
return createTask({
name,
project_id: projectId,
+ result_description: description,
iteration_id: iteration,
iteration_stage: stage,
type: TASKTYPES.TRAINING,
- docker_image_config: config,
+ docker_image_config: { ...config, openpai_enable: openpai, },
+ preprocess,
parameters: {
strategy,
dataset_id: datasetId,
@@ -195,27 +268,32 @@ export function createTrainTask({
network,
train_type: trainType,
model_id: model,
+ model_stage_id: stageId,
docker_image: image,
docker_image_id: imageId,
}
})
}
-export function createMiningTask({
- iteration, stage,
- projectId, datasetId, model, topk, algorithm,
+export function mine({
+ iteration, stage, openpai, description,
+ projectId, datasetId, modelStage = [], topk, algorithm,
config, strategy, inference, name, image, imageId,
}) {
+ const model = modelStage[0]
+ const stageId = modelStage[1]
return createTask({
type: TASKTYPES.MINING,
project_id: projectId,
+ result_description: description,
iteration_id: iteration,
iteration_stage: stage,
name,
- docker_image_config: config,
+ docker_image_config: { ...config, openpai_enable: openpai, },
parameters: {
strategy,
model_id: model,
+ model_stage_id: stageId,
dataset_id: datasetId,
mining_algorithm: algorithm,
top_k: topk,
@@ -231,35 +309,38 @@ export function createMiningTask({
* @param {object} task {
* {string} name
* {number} projectId
- * {number} datasetId
+ * {array} datasets
* {object} config
- * {number} model
+ * {array>} stages
* {string} image
* {string} imageId
* {string} description
* }
* @returns
*/
-export function createInferenceTask({
+export function infer({
name,
projectId,
- datasetId,
- model = [],
+ datasets,
+ stages = [],
config,
image,
imageId,
+ openpai,
description,
}) {
- const params = model.map(md => ({
+ const maps = datasets.map(dataset => stages.map(([model, stage]) => ({ dataset, model, stage }))).flat()
+ const params = maps.map(({ model, stage, dataset }) => ({
name,
type: TASKTYPES.INFERENCE,
project_id: projectId,
- description,
- docker_image_config: config,
+ result_description: description,
+ docker_image_config: { ...config, openpai_enable: openpai, },
parameters: {
- model_id: md,
+ model_id: model,
+ model_stage_id: stage,
generate_annotations: true,
- dataset_id: datasetId,
+ dataset_id: dataset,
docker_image: image,
docker_image_id: imageId,
}
diff --git a/ymir/web/src/services/user.js b/ymir/web/src/services/user.js
index 8c6c591913..5eab598bcc 100644
--- a/ymir/web/src/services/user.js
+++ b/ymir/web/src/services/user.js
@@ -6,13 +6,15 @@ function sha1(value) {
return CryptoJS.SHA1(value).toString()
}
-export function signup({ email, username, password, phone = null }) {
+export function signup({ email, username, password, phone = null, organization, scene }) {
password = sha1(password)
return request.post("/users/", {
email,
username,
password,
phone,
+ organization,
+ scene,
})
}
@@ -82,7 +84,7 @@ export function getUsers(params) {
*/
export function setUserState({ id, state, role }) {
return request({
- url: `/users/${id}`,
+ url: `/users/${id}`,
method: 'PATCH',
data: { state, role },
})
diff --git a/ymir/web/src/utils/__test__/number.test.js b/ymir/web/src/utils/__test__/number.test.js
index 61a3d3668b..5ed2f89bef 100644
--- a/ymir/web/src/utils/__test__/number.test.js
+++ b/ymir/web/src/utils/__test__/number.test.js
@@ -4,10 +4,10 @@ describe("utils: number", () => {
it("function: humanize. humanize number.", () => {
const numbers = [
{ value: 324, expected: '324' },
- { value: 3324, expected: '3K' },
- { value: 395324, expected: '395K' },
- { value: 12395324, expected: '12M' },
- { value: 2425435324, expected: '2B' },
+ { value: 3324, expected: '3k' },
+ { value: 395324, expected: '395k' },
+ { value: 12395324, expected: '12m' },
+ { value: 2425435324, expected: '2b' },
]
numbers.forEach(num => expect(humanize(num.value)).toBe(num.expected))
expect(humanize()).toBe(0)
diff --git a/ymir/web/src/utils/__test__/request.test.js b/ymir/web/src/utils/__test__/request.test.js
index 1845b9ab6f..10d42ef1af 100644
--- a/ymir/web/src/utils/__test__/request.test.js
+++ b/ymir/web/src/utils/__test__/request.test.js
@@ -149,7 +149,7 @@ describe("utils: request", () => {
// 400 -> 1003
const error4001003Result = reqHandler.rejected(error4001003)
expect(msgSpy).toHaveBeenCalled()
- expect(error4001003Result).toBe('error1003')
+ expect(error4001003Result).toEqual({ code: 1003 })
// 400 -> 110104
const error400110104Result = reqHandler.rejected(error400110104)
@@ -158,14 +158,12 @@ describe("utils: request", () => {
expect(error400110104Result).toBeUndefined()
// 405
- expect(() => {
- reqHandler.rejected(error405)
- }).toThrow()
+ const error405Result = reqHandler.rejected(error405)
+ expect(error405Result).toEqual({ code: 405 })
// 504
- expect(() => {
- reqHandler.rejected(error504)
- }).toThrow()
+ const error504Result = reqHandler.rejected(error504)
expect(msgSpy).toHaveBeenCalled()
+ expect(error504Result).toEqual({ code: 504 })
})
})
diff --git a/ymir/web/src/utils/number.ts b/ymir/web/src/utils/number.ts
index 808b263df6..a3f0b69aed 100644
--- a/ymir/web/src/utils/number.ts
+++ b/ymir/web/src/utils/number.ts
@@ -6,7 +6,7 @@ export function humanize(num: number | string) {
if (isNaN(Number(num))) {
return num
}
- const units = ['', 'K', 'M', 'B']
+ const units = ['', 'k', 'm', 'b']
num = typeof num == 'string' ? parseFloat(num) : num
num = num.toLocaleString()
const ell = num.match(/,/ig)
@@ -28,7 +28,11 @@ export function randomNumber(count = 6) {
* @returns
*/
export function randomBetween(n: number, m: number, exclude: number): number {
- const result = Math.min(m, n) + Math.floor(Math.random() * Math.abs(m - n))
+ const gap = Math.abs(m - n)
+ if (!gap) {
+ return gap
+ }
+ const result = Math.min(m, n) + Math.floor(Math.random() * gap)
if (result === exclude) {
return randomBetween(n, m, exclude)
diff --git a/ymir/web/src/utils/object.ts b/ymir/web/src/utils/object.ts
index 9091c0eefd..caffa0ac40 100644
--- a/ymir/web/src/utils/object.ts
+++ b/ymir/web/src/utils/object.ts
@@ -1 +1,9 @@
export const deepClone = (obj: object) => JSON.parse(JSON.stringify(obj))
+
+type obj = {
+ [key: string]: any,
+}
+export const isSame = (obj1: obj, obj2: obj) => {
+ const same = (o1:obj, o2: obj) => Object.keys(o1).every(key => o1[key] === o2[key])
+ return same(obj1, obj2) && same(obj2, obj1)
+}
diff --git a/ymir/web/src/utils/request.js b/ymir/web/src/utils/request.js
index 4bf08eeedb..c9c3a1e261 100644
--- a/ymir/web/src/utils/request.js
+++ b/ymir/web/src/utils/request.js
@@ -13,8 +13,6 @@ const getBaseURL = () => {
return window.baseConfig?.APIURL || envUrl
}
-// console.log('base url: ', getBaseURL(), process.env)
-
const request = axios.create({
baseURL: getBaseURL(),
// timeout: 1,
@@ -37,7 +35,7 @@ request.interceptors.response.use(
(res) => {
if (res.data.code !== 0) {
message.error(t(`error${res.data.code}`))
- if (res.data.code === 110104) {
+ if ([110104, 110112].includes(res.data.code)) {
return logout()
}
}
@@ -50,18 +48,20 @@ request.interceptors.response.use(
return logout()
} else if (err.request.status === 504) {
message.error(t('error.timeout'))
+ } else if (err.request.status === 502) {
+ message.error(t('error.502'))
} else {
const res = err.response
- if (res && res.data && res.data.code) {
+ if (res?.data?.code) {
if (res.data.code === 110104) {
return logout()
}
- return message.error(t(`error${res.data.code}`))
+ message.error(t(`error${res.data.code}`))
+ return res.data
}
}
- console.error(err.request.statusText)
- throw new Error(err)
+ return { code: err.request.status }
}
)
diff --git a/ymir/web/src/utils/string.ts b/ymir/web/src/utils/string.ts
index 8951900e15..04fdad32b6 100644
--- a/ymir/web/src/utils/string.ts
+++ b/ymir/web/src/utils/string.ts
@@ -19,5 +19,14 @@ export function string2Array(str: string, seprate = ',') {
return
}
const arr = str.split(seprate)
- return arr.map(item => Number.isNaN(Number(item)) ? item : Number(item))
+ return arr.map(item => Number.isNaN(Number(item)) ? item : Number(item)).filter(i => i)
+}
+
+export const getRandomRGB = (level = 1) => {
+ const units = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
+ const random = (list: Array) => {
+ const index = Math.floor(Math.random() * list.length / level) * level
+ return list[index]
+ }
+ return `#${random(units)}${random(units)}${random(units)}`
}
diff --git a/ymir/web/src/utils/t.ts b/ymir/web/src/utils/t.ts
index 476877747e..e964baae9b 100644
--- a/ymir/web/src/utils/t.ts
+++ b/ymir/web/src/utils/t.ts
@@ -24,7 +24,7 @@ export const initIntl = (prefix: string = '') => {
return _helper
}
-const showIntl = (id: string, values = {}, prefix: string) => {
+const showIntl = (id: string, values = {}, prefix: string = '') => {
if (!id) {
return
}
diff --git a/ymir/web/src/utils/url.ts b/ymir/web/src/utils/url.ts
new file mode 100644
index 0000000000..3636e37e28
--- /dev/null
+++ b/ymir/web/src/utils/url.ts
@@ -0,0 +1,8 @@
+export function encode(obj: { [name: string]: string | number }) {
+ return Object.keys(obj).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
+}
+
+export function decode(paramsString: string) {
+ const params = new URLSearchParams(paramsString)
+ return [...params].reduce((prev, [key, value]) => ({ ...prev, [key]: value }), {})
+}
\ No newline at end of file