Merge pull request #451 from c-bata/optional-text-widget

Allow `TextInputWidget` to accept strings for user attribute forms.
This commit is contained in:
Masashi Shibata
2023-05-10 10:02:14 +09:00
committed by GitHub
10 changed files with 522 additions and 422 deletions
+12 -1
View File
@@ -46,7 +46,12 @@ if TYPE_CHECKING:
)
TextInputWidgetJSON = TypedDict(
"TextInputWidgetJSON",
{"type": Literal["text"], "description": Optional[str], "user_attr_key": Optional[str]},
{
"type": Literal["text"],
"description": Optional[str],
"user_attr_key": Optional[str],
"optional": bool,
},
)
UserAttrRefJSON = TypedDict("UserAttrRefJSON", {"type": Literal["user_attr"], "key": str})
FormWidgetJSON = TypedDict(
@@ -185,6 +190,7 @@ class TextInputWidget:
description: A description of the text input field.
user_attr_key: The key used by `register_user_attr_form_widgets`.
Form output is saved as `trial.user_attrs[user_attr_key]`. Defaults to None.
optional: If True, an empty string is acceptable.
Example:
.. code-block:: python
@@ -197,6 +203,7 @@ class TextInputWidget:
description: Optional[str] = None
user_attr_key: Optional[str] = None
optional: bool = False
def to_dict(self) -> TextInputWidgetJSON:
"""
@@ -209,6 +216,7 @@ class TextInputWidget:
"type": "text",
"description": self.description,
"user_attr_key": self.user_attr_key,
"optional": self.optional,
}
@classmethod
@@ -217,6 +225,7 @@ class TextInputWidget:
return cls(
description=d.get("description"),
user_attr_key=d.get("user_attr_key"),
optional=d.get("optional", False),
)
@@ -347,6 +356,8 @@ def register_objective_form_widgets(
not isinstance(w, ObjectiveUserAttrRef) and w.user_attr_key is not None for w in widgets
):
warnings.warn("`user_attr_key` specified, but it will not be used.")
if any(isinstance(w, TextInputWidget) and w.optional for w in widgets):
raise ValueError("TextInputWidget.optional must be False.")
form_widgets: FormWidgetJSON = {
"output_type": "objective",
"widgets": [w.to_dict() for w in widgets],
+2
View File
@@ -91,6 +91,8 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]:
value: str
if isinstance(v, bytes):
value = "<binary object>"
elif isinstance(v, str):
value = v
else:
value = json.dumps(v)
value = value[:MAX_ATTR_LENGTH] if len(value) > MAX_ATTR_LENGTH else value
+17 -3
View File
@@ -23,6 +23,7 @@ import {
isFileUploading,
artifactIsAvailable,
reloadIntervalState,
trialsUpdatingState,
} from "./state"
import { getDominatedTrials } from "./dominatedTrials"
@@ -45,6 +46,7 @@ export const actionCreator = () => {
const [paramImportance, setParamImportance] =
useRecoilState<StudyParamImportance>(paramImportanceState)
const setUploading = useSetRecoilState<boolean>(isFileUploading)
const setTrialsUpdating = useSetRecoilState(trialsUpdatingState)
const setArtifactIsAvailable = useSetRecoilState<boolean>(artifactIsAvailable)
const setStudyDetailState = (studyId: number, study: StudyDetail) => {
@@ -62,6 +64,13 @@ export const actionCreator = () => {
newStudy.trials = newTrials
setStudyDetailState(studyId, newStudy)
}
const setTrialUpdating = (trialId: number, updating: boolean) => {
setTrialsUpdating((prev) => {
const newVal = Object.assign({}, prev)
newVal[trialId] = updating
return newVal
})
}
const setTrialNote = (studyId: number, index: number, note: Note) => {
const newTrial: Trial = Object.assign(
@@ -157,7 +166,7 @@ export const actionCreator = () => {
const setTrialUserAttrs = (
studyId: number,
index: number,
user_attrs: { [key: string]: number }
user_attrs: { [key: string]: number | string }
) => {
const newTrial: Trial = Object.assign(
{},
@@ -469,6 +478,7 @@ export const actionCreator = () => {
const makeTrialFail = (studyId: number, trialId: number): void => {
const message = `id=${trialId}, state=Fail`
setTrialUpdating(trialId, true)
tellTrialAPI(trialId, "Fail")
.then(() => {
const index = studyDetails[studyId].trials.findIndex(
@@ -486,6 +496,7 @@ export const actionCreator = () => {
})
})
.catch((err) => {
setTrialUpdating(trialId, false)
const reason = err.response?.data.reason
enqueueSnackbar(
`Failed to update trial (${message}). Reason: ${reason}`,
@@ -503,6 +514,7 @@ export const actionCreator = () => {
values: number[]
): void => {
const message = `id=${trialId}, state=Complete, values=${values}`
setTrialUpdating(trialId, true)
tellTrialAPI(trialId, "Complete", values)
.then(() => {
const index = studyDetails[studyId].trials.findIndex(
@@ -517,6 +529,7 @@ export const actionCreator = () => {
setTrialStateValues(studyId, index, "Complete", values)
})
.catch((err) => {
setTrialUpdating(trialId, false)
const reason = err.response?.data.reason
enqueueSnackbar(
`Failed to update trial (${message}). Reason: ${reason}`,
@@ -531,10 +544,10 @@ export const actionCreator = () => {
const saveTrialUserAttrs = (
studyId: number,
trialId: number,
user_attrs: { [key: string]: number }
user_attrs: { [key: string]: string | number }
): void => {
console.log("user_attrs", user_attrs)
const message = `id=${trialId}, user_attrs=${JSON.stringify(user_attrs)}`
setTrialUpdating(trialId, true)
saveTrialUserAttrsAPI(trialId, user_attrs)
.then(() => {
const index = studyDetails[studyId].trials.findIndex(
@@ -552,6 +565,7 @@ export const actionCreator = () => {
})
})
.catch((err) => {
setTrialUpdating(trialId, false)
const reason = err.response?.data.reason
enqueueSnackbar(
`Failed to update trial (${message}). Reason: ${reason}`,
+1 -1
View File
@@ -283,7 +283,7 @@ export const tellTrialAPI = (
export const saveTrialUserAttrsAPI = (
trialId: number,
user_attrs: { [key: string]: number }
user_attrs: { [key: string]: number | string }
): Promise<void> => {
const req = { user_attrs: user_attrs }
@@ -1,385 +0,0 @@
import React, { FC, useMemo, useState } from "react"
import {
Typography,
Box,
useTheme,
Card,
FormControlLabel,
FormControl,
FormLabel,
Button,
RadioGroup,
Radio,
Slider,
TextField,
} from "@mui/material"
import { DebouncedInputTextField } from "./Debounce"
import { actionCreator } from "../action"
export const ObjectiveForm: FC<{
trial: Trial
directions: StudyDirection[]
names: string[]
formWidgets: FormWidgets
}> = ({ trial, directions, names, formWidgets }) => {
const theme = useTheme()
const action = actionCreator()
const [values, setValues] = useState<(number | null)[]>(
formWidgets.widgets.map((widget) => {
if (widget === undefined) {
return null
} else if (widget.type === "text") {
return null
} else if (widget.type === "choice") {
return widget.values.at(0) || null
} else if (widget.type === "slider") {
return widget.min
} else if (widget.type === "user_attr") {
const attr = trial.user_attrs.find((attr) => attr.key == widget.key)
if (attr === undefined) {
return null
} else {
const n = Number(attr.value)
return isNaN(n) ? null : n
}
} else {
return null
}
})
)
const setValue = (objectiveId: number, value: number | null) => {
const newValues = [...values]
if (newValues.length <= objectiveId) {
return
}
newValues[objectiveId] = value
setValues(newValues)
}
const disableSubmit = useMemo<boolean>(
() => values.findIndex((v) => v === null) >= 0,
[values]
)
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>): void => {
e.preventDefault()
if (formWidgets.output_type == "objective") {
const filtered = values.filter<number>((v): v is number => v !== null)
if (filtered.length !== directions.length) {
return
}
action.makeTrialComplete(trial.study_id, trial.trial_id, filtered)
} else if (formWidgets.output_type == "user_attr") {
const user_attrs = Object.fromEntries(
formWidgets.widgets.map((widget, i) => [
widget.user_attr_key,
values[i],
])
)
action.saveTrialUserAttrs(trial.study_id, trial.trial_id, user_attrs)
}
}
const getMetricName = (i: number): string => {
if (formWidgets.output_type == "objective") {
const n = names.at(i)
if (n !== undefined) {
return n
}
if (directions.length == 1) {
return `Objective`
} else {
return `Objective ${i}`
}
} else if (formWidgets.output_type == "user_attr") {
return formWidgets.widgets[i].user_attr_key as string
}
return "Unkown metric name"
}
return (
<>
<Typography
variant="h5"
sx={{ fontWeight: theme.typography.fontWeightBold }}
>
{directions.length > 1 ? "Set Objective Values" : "Set Objective Value"}
</Typography>
<Box sx={{ p: theme.spacing(1, 0) }}>
<Card
sx={{
display: "flex",
flexDirection: "column",
marginBottom: theme.spacing(2),
margin: theme.spacing(0, 1, 1, 0),
p: theme.spacing(1),
}}
>
{formWidgets.widgets.map((widget, i) => {
const value = values.at(i)
const key = `objective-${i}`
if (widget.type === "text") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getMetricName(i)} - {widget.description}
</FormLabel>
<DebouncedInputTextField
onChange={(s, valid) => {
const n = Number(s)
if (s.length > 0 && valid && !isNaN(n)) {
setValue(i, n)
return
} else if (values.at(i) !== null) {
setValue(i, null)
}
}}
delay={500}
textFieldProps={{
required: true,
autoFocus: true,
fullWidth: true,
helperText:
value === null || value === undefined
? `Please input the float number.`
: "",
type: "text",
inputProps: {
pattern: "[-+]?[0-9]*.?[0-9]+([eE][-+]?[0-9]+)?",
},
}}
/>
</FormControl>
)
} else if (widget.type === "choice") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getMetricName(i)} - {widget.description}
</FormLabel>
<RadioGroup row defaultValue={widget.values.at(0)}>
{widget.choices.map((c, j) => (
<FormControlLabel
key={c}
control={
<Radio
checked={value === widget.values.at(j)}
onChange={(e) => {
const selected = widget.values.at(j)
if (e.target.checked) {
setValue(
i,
selected === undefined ? null : selected
)
}
}}
/>
}
label={c}
/>
))}
</RadioGroup>
</FormControl>
)
} else if (widget.type === "slider") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getMetricName(i)} - {widget.description}
</FormLabel>
<Box sx={{ padding: theme.spacing(0, 2) }}>
<Slider
onChange={(e) => {
// @ts-ignore
setValue(i, e.target.value as number)
}}
defaultValue={widget.min}
min={widget.min}
max={widget.max}
step={widget.step}
marks={
widget.labels === null || widget.labels.length == 0
? true
: widget.labels
}
valueLabelDisplay="auto"
/>
</Box>
</FormControl>
)
} else if (widget.type === "user_attr") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>{getMetricName(i)}</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={value || undefined}
error={value === null}
helperText={
value === null || value === undefined
? `This objective value is referred from trial.user_attrs[${widget.key}].`
: ""
}
/>
</FormControl>
)
}
return null
})}
<Box
sx={{
display: "flex",
flexDirection: "row",
margin: theme.spacing(1, 2),
}}
>
<Button
variant="contained"
type="submit"
sx={{ marginRight: theme.spacing(1) }}
disabled={disableSubmit}
onClick={handleSubmit}
>
Submit
</Button>
<Box sx={{ flexGrow: 1 }} />
<Button
variant="outlined"
color="error"
onClick={() => {
action.makeTrialFail(trial.study_id, trial.trial_id)
}}
>
Fail Trial
</Button>
</Box>
</Card>
</Box>
</>
)
}
export const ReadonlyObjectiveForm: FC<{
trial: Trial
directions: StudyDirection[]
names: string[]
formWidgets: FormWidgets
}> = ({ trial, directions, names, formWidgets }) => {
const theme = useTheme()
const getMetricName = (i: number): string => {
if (formWidgets.output_type == "objective") {
const n = names.at(i)
if (n !== undefined) {
return n
}
if (directions.length == 1) {
return `Objective`
} else {
return `Objective ${i}`
}
} else if (formWidgets.output_type == "user_attr") {
return formWidgets.widgets[i].user_attr_key as string
}
return "Unkown metric name"
}
return (
<>
<Typography
variant="h5"
sx={{ fontWeight: theme.typography.fontWeightBold }}
>
{directions.length > 1 ? "Set Objective Values" : "Set Objective Value"}
</Typography>
<Box sx={{ p: theme.spacing(1, 0) }}>
<Card
sx={{
display: "flex",
flexDirection: "column",
marginBottom: theme.spacing(2),
margin: theme.spacing(0, 1, 1, 0),
p: theme.spacing(1),
}}
>
{formWidgets.widgets.map((widget, i) => {
const key = `objective-${i}`
if (widget.type === "text") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getMetricName(i)} - {widget.description}
</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={trial.values?.at(i)}
/>
</FormControl>
)
} else if (widget.type === "choice") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getMetricName(i)} - {widget.description}
</FormLabel>
<RadioGroup row defaultValue={trial.values?.at(i)}>
{widget.choices.map((c, j) => (
<FormControlLabel
key={c}
control={
<Radio
checked={
trial.values?.at(i) === widget.values.at(j)
}
/>
}
label={c}
disabled
/>
))}
</RadioGroup>
</FormControl>
)
} else if (widget.type === "slider") {
const value = trial.values?.at(i)
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getMetricName(i)} - {widget.description}
</FormLabel>
<Box sx={{ padding: theme.spacing(0, 2) }}>
<Slider
defaultValue={
value === "inf" || value === "-inf" ? undefined : value
}
min={widget.min}
max={widget.max}
step={widget.step}
marks={
widget.labels === null || widget.labels.length == 0
? true
: widget.labels
}
valueLabelDisplay="auto"
disabled
/>
</Box>
</FormControl>
)
} else if (widget.type === "user_attr") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>{getMetricName(i)}</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={trial.values?.at(i)}
/>
</FormControl>
)
}
return null
})}
</Card>
</Box>
</>
)
}
@@ -0,0 +1,470 @@
import React, { FC, ReactNode, useMemo, useState } from "react"
import {
Typography,
Box,
useTheme,
Card,
FormControlLabel,
FormControl,
FormLabel,
Button,
RadioGroup,
Radio,
Slider,
TextField,
} from "@mui/material"
import { DebouncedInputTextField } from "./Debounce"
import { actionCreator } from "../action"
import { useTrialUpdatingValue } from "../state"
type WidgetState = {
isValid: boolean
value: number | string
render: () => ReactNode
}
export const TrialFormWidgets: FC<{
trial: Trial
objectiveNames: string[]
directions: StudyDirection[]
formWidgets?: FormWidgets
}> = ({ trial, objectiveNames, directions, formWidgets }) => {
if (
formWidgets === undefined ||
trial.state === "Pruned" ||
trial.state === "Fail"
) {
return null
}
const theme = useTheme()
const trialNowUpdating = useTrialUpdatingValue(trial.trial_id)
const headerText =
formWidgets.output_type === "user_attr"
? "Set User Attributes Form"
: directions.length > 1
? "Set Objective Values Form"
: "Set Objective Value Form"
const widgetNames = formWidgets.widgets.map((widget, i) => {
if (formWidgets.output_type == "objective") {
if (objectiveNames.at(i) !== undefined) {
return objectiveNames[i]
}
return directions.length == 1 ? "Objective" : `Objective ${i}`
} else if (formWidgets.output_type == "user_attr") {
if (widget.type !== "user_attr" && widget.user_attr_key !== undefined) {
return widget.user_attr_key
}
}
console.error("Must not reach here")
return "Unknown"
})
return (
<>
<Typography
variant="h5"
sx={{ fontWeight: theme.typography.fontWeightBold }}
>
{headerText}
</Typography>
{trial.state === "Running" && !trialNowUpdating ? (
<UpdatableFormWidgets
trial={trial}
widgetNames={widgetNames}
formWidgets={formWidgets}
/>
) : (
<ReadonlyFormWidgets
trial={trial}
widgetNames={widgetNames}
formWidgets={formWidgets}
/>
)}
</>
)
}
const UpdatableFormWidgets: FC<{
trial: Trial
widgetNames: string[]
formWidgets: FormWidgets
}> = ({ trial, widgetNames, formWidgets }) => {
const theme = useTheme()
const action = actionCreator()
const widgetStates = formWidgets.widgets
.map((w, i) => {
const key = `${formWidgets.output_type}-${i}`
const outputType = formWidgets.output_type
if (w.type === "text") {
return useTextInputWidget(key, outputType, w, widgetNames[i])
} else if (w.type === "choice") {
return useChoiceWidget(key, outputType, w, widgetNames[i])
} else if (w.type === "slider") {
return useSliderWidget(key, outputType, w, widgetNames[i])
} else if (w.type === "user_attr") {
return useUserAttrRefWidget(key, w, widgetNames[i], trial)
}
console.error("Must not reach here")
return undefined
})
.filter((w): w is WidgetState => w !== undefined)
const disableSubmit = useMemo<boolean>(
() => !widgetStates.every((ws) => ws.isValid),
[widgetStates]
)
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>): void => {
e.preventDefault()
const values = widgetStates.map((ws) => ws.value)
if (formWidgets.output_type == "objective") {
const filtered = values.filter<number>((v): v is number => v !== null)
if (filtered.length !== formWidgets.widgets.length) {
return
}
action.makeTrialComplete(trial.study_id, trial.trial_id, filtered)
} else if (formWidgets.output_type == "user_attr") {
const user_attrs = Object.fromEntries(
formWidgets.widgets.map((widget, i) => [
widget.user_attr_key,
values[i] !== null ? values[i] : "",
])
)
action.saveTrialUserAttrs(trial.study_id, trial.trial_id, user_attrs)
}
}
return (
<Box sx={{ p: theme.spacing(1, 0) }}>
<Card
sx={{
display: "flex",
flexDirection: "column",
marginBottom: theme.spacing(2),
margin: theme.spacing(0, 1, 1, 0),
p: theme.spacing(1),
}}
>
{widgetStates.map((ws) => ws.render())}
<Box
sx={{
display: "flex",
flexDirection: "row",
margin: theme.spacing(1, 2),
}}
>
<Button
variant="contained"
type="submit"
sx={{ marginRight: theme.spacing(1) }}
disabled={disableSubmit}
onClick={handleSubmit}
>
Submit
</Button>
<Box sx={{ flexGrow: 1 }} />
<Button
variant="outlined"
color="error"
onClick={() => {
action.makeTrialFail(trial.study_id, trial.trial_id)
}}
>
Fail Trial
</Button>
</Box>
</Card>
</Box>
)
}
export const useTextInputWidget = (
key: string,
widgetType: "user_attr" | "objective",
widget: ObjectiveTextInputWidget,
metricName: string
): WidgetState => {
const theme = useTheme()
const [value, setValue] = useState<number | string>("")
const isValid = useMemo(
() =>
widgetType === "user_attr"
? value !== "" || widget.optional
: value !== "" && !isNaN(Number(value)),
[widget, value]
)
const inputProps =
widgetType === "objective"
? {
pattern: "[-+]?[0-9]*.?[0-9]+([eE][-+]?[0-9]+)?",
}
: undefined
const helperText =
!widget.optional && value === "" ? `Please input the float number.` : ""
const render = () => (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{metricName} - {widget.description}
</FormLabel>
<DebouncedInputTextField
onChange={(s, valid) => {
if (widgetType === "user_attr") {
setValue(s)
return
}
const n = Number(s)
if (s.length > 0 && valid && !isNaN(n)) {
setValue(n)
} else {
setValue("")
}
}}
delay={500}
textFieldProps={{
type: "text",
autoFocus: true,
fullWidth: true,
required: !widget.optional,
helperText,
inputProps,
}}
/>
</FormControl>
)
return { isValid, value, render }
}
export const useChoiceWidget = (
key: string,
widgetType: "user_attr" | "objective",
widget: ObjectiveChoiceWidget,
metricName: string
): WidgetState => {
const theme = useTheme()
const [value, setValue] = useState<number>(widget.values[0])
const render = () => (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{metricName} - {widget.description}
</FormLabel>
<RadioGroup row defaultValue={widget.values.at(0)}>
{widget.choices.map((c, j) => (
<FormControlLabel
key={c}
control={
<Radio
checked={value === widget.values.at(j)}
onChange={(e) => {
const selected = widget.values.at(j)
if (selected === undefined) {
console.error("Must not reach here.")
return
}
if (e.target.checked) {
setValue(selected)
}
}}
/>
}
label={c}
/>
))}
</RadioGroup>
</FormControl>
)
return { isValid: true, value, render }
}
export const useSliderWidget = (
key: string,
widgetType: "user_attr" | "objective",
widget: ObjectiveSliderWidget,
metricName: string
): WidgetState => {
const theme = useTheme()
const [value, setValue] = useState<number>(widget.min)
const render = () => (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{metricName} - {widget.description}
</FormLabel>
<Box sx={{ padding: theme.spacing(0, 2) }}>
<Slider
onChange={(e) => {
// @ts-ignore
setValue(e.target.value as number)
}}
defaultValue={widget.min}
min={widget.min}
max={widget.max}
step={widget.step}
marks={
widget.labels === null || widget.labels.length == 0
? true
: widget.labels
}
valueLabelDisplay="auto"
/>
</Box>
</FormControl>
)
return { isValid: true, value, render }
}
export const useUserAttrRefWidget = (
key: string,
widget: ObjectiveUserAttrRef,
metricName: string,
trial: Trial
): WidgetState => {
const theme = useTheme()
const value = useMemo(() => {
const attr = trial.user_attrs.find((attr) => attr.key === widget.key)
if (attr === undefined) {
return null
}
const n = Number(attr.value)
if (isNaN(n)) {
return null
}
return n
}, [trial.user_attrs])
const render = () => (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>{metricName}</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={value || ""}
error={value === null}
helperText={
value === null
? `This objective value is referred from trial.user_attrs[${widget.key}].`
: ""
}
/>
</FormControl>
)
return {
isValid: value !== null,
value: value !== null ? value : "",
render,
}
}
const ReadonlyFormWidgets: FC<{
trial: Trial
widgetNames: string[]
formWidgets: FormWidgets
}> = ({ trial, widgetNames, formWidgets }) => {
const theme = useTheme()
const getValue = (i: number): string | TrialValueNumber => {
if (formWidgets.output_type === "user_attr") {
const widget = formWidgets.widgets[i] as UserAttrFormWidget
return (
trial.user_attrs.find((attr) => attr.key === widget.user_attr_key)
?.value || ""
)
}
const value = trial.values?.at(i)
if (value === undefined) {
console.error("Must not reach here.")
return 0
}
return value
}
return (
<Box sx={{ p: theme.spacing(1, 0) }}>
<Card
sx={{
display: "flex",
flexDirection: "column",
marginBottom: theme.spacing(2),
margin: theme.spacing(0, 1, 1, 0),
p: theme.spacing(1),
}}
>
{formWidgets.widgets.map((widget, i) => {
const key = `objective-${i}`
const widgetName = widgetNames[i]
if (widget.type === "text") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{widgetName} - {widget.description}
</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={getValue(i)}
/>
</FormControl>
)
} else if (widget.type === "choice") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{widgetName} - {widget.description}
</FormLabel>
<RadioGroup row defaultValue={trial.values?.at(i)}>
{widget.choices.map((c, j) => (
<FormControlLabel
key={c}
control={
<Radio
checked={trial.values?.at(i) === widget.values.at(j)}
/>
}
label={c}
disabled
/>
))}
</RadioGroup>
</FormControl>
)
} else if (widget.type === "slider") {
const value = trial.values?.at(i)
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{widgetName} - {widget.description}
</FormLabel>
<Box sx={{ padding: theme.spacing(0, 2) }}>
<Slider
defaultValue={
value === "inf" || value === "-inf" ? undefined : value
}
min={widget.min}
max={widget.max}
step={widget.step}
marks={
widget.labels === null || widget.labels.length == 0
? true
: widget.labels
}
valueLabelDisplay="auto"
disabled
/>
</Box>
</FormControl>
)
} else if (widget.type === "user_attr") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>{widgetName}</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={trial.values?.at(i)}
disabled
/>
</FormControl>
)
}
return null
})}
</Card>
</Box>
)
}
+7 -21
View File
@@ -42,7 +42,7 @@ import { useRecoilValue } from "recoil"
import { artifactIsAvailable } from "../state"
import { actionCreator } from "../action"
import { useDeleteArtifactDialog } from "./DeleteArtifactDialog"
import { ObjectiveForm, ReadonlyObjectiveForm } from "./ObjectiveForm"
import { TrialFormWidgets } from "./TrialFormWidgets"
const states: TrialState[] = [
"Complete",
@@ -274,26 +274,12 @@ const TrialListDetail: FC<{
latestNote={trial.note}
cardSx={{ marginBottom: theme.spacing(2) }}
/>
{trial.state === "Running" &&
directions.length > 0 &&
formWidgets !== undefined && (
<ObjectiveForm
trial={trial}
directions={directions}
names={objectiveNames}
formWidgets={formWidgets}
/>
)}
{trial.state === "Complete" &&
directions.length > 0 &&
formWidgets !== undefined && (
<ReadonlyObjectiveForm
trial={trial}
directions={directions}
names={objectiveNames}
formWidgets={formWidgets}
/>
)}
<TrialFormWidgets
trial={trial}
directions={directions}
objectiveNames={objectiveNames}
formWidgets={formWidgets}
/>
<Box
sx={{
marginBottom: theme.spacing(2),
+12
View File
@@ -10,6 +10,13 @@ export const studyDetailsState = atom<StudyDetails>({
default: {},
})
export const trialsUpdatingState = atom<{
[trialId: string]: boolean
}>({
key: "trialsUpdating",
default: {},
})
export const paramImportanceState = atom<StudyParamImportance>({
key: "paramImportance",
default: {},
@@ -59,6 +66,11 @@ export const useStudySummaryValue = (studyId: number): StudySummary | null => {
return studySummaries.find((s) => s.study_id == studyId) || null
}
export const useTrialUpdatingValue = (trialId: number): boolean => {
const updating = useRecoilValue(trialsUpdatingState)
return updating[trialId] || false
}
export const useParamImportanceValue = (
studyId: number
): ParamImportance[][] | null => {
+1
View File
@@ -151,6 +151,7 @@ type ObjectiveSliderWidget = {
type ObjectiveTextInputWidget = {
type: "text"
description: string
optional: boolean
user_attr_key?: string
}
-11
View File
@@ -10,17 +10,6 @@ class SerializeAttrsTestCase(TestCase):
serialized = serialize_attrs({"bytes": b"This is a bytes object."})
self.assertEqual(serialized[0]["value"], "<binary object>")
def test_serialize_string(self) -> None:
for length in [1000, 1024, 1100]:
with self.subTest(f"length: {length}"):
value = "a" * length
serialized = serialize_attrs(
{
"key": value,
}
)
self.assertLessEqual(len(serialized[0]["value"]), 1024)
def test_serialize_dict(self) -> None:
serialized = serialize_attrs(
{