Merge pull request #431 from c-bata/follow-up-411

Follow-up #411 to refactor type definitions
This commit is contained in:
Masashi Shibata
2023-04-13 12:35:50 +09:00
committed by GitHub
11 changed files with 381 additions and 256 deletions
+3 -3
View File
@@ -23,9 +23,9 @@ Human-in-the-loop
:nosignatures:
optuna_dashboard.register_objective_form_widgets
optuna_dashboard.ObjectiveChoiceWidget
optuna_dashboard.ObjectiveSliderWidget
optuna_dashboard.ObjectiveTextInputWidget
optuna_dashboard.ChoiceWidget
optuna_dashboard.SliderWidget
optuna_dashboard.TextInputWidget
optuna_dashboard.ObjectiveUserAttrRef
Artifact
+9 -5
View File
@@ -1,13 +1,17 @@
from ._app import run_server # noqa
from ._app import wsgi # noqa
from ._form_widget import ChoiceWidget # noqa
from ._form_widget import ObjectiveChoiceWidget # noqa
from ._form_widget import ObjectiveSliderWidget # noqa
from ._form_widget import ObjectiveTextInputWidget # noqa
from ._form_widget import ObjectiveUserAttrRef # noqa
from ._form_widget import register_objective_form_widgets # noqa
from ._form_widget import register_user_attr_form_widgets # noqa
from ._form_widget import SliderWidget # noqa
from ._form_widget import TextInputWidget # noqa
from ._named_objectives import set_objective_names # noqa
from ._note import get_note # noqa
from ._note import save_note # noqa
from ._objective_form_widget import ObjectiveChoiceWidget # noqa
from ._objective_form_widget import ObjectiveSliderWidget # noqa
from ._objective_form_widget import ObjectiveTextInputWidget # noqa
from ._objective_form_widget import ObjectiveUserAttrRef # noqa
from ._objective_form_widget import register_objective_form_widgets # noqa
__version__ = "0.9.0"
+18
View File
@@ -447,6 +447,24 @@ def create_app(
response.status = 204
return {}
@app.post("/api/trials/<trial_id:int>/user-attrs")
@json_api_view
def save_trial_user_attrs(trial_id: int) -> dict[str, Any]:
user_attrs = request.json.get("user_attrs", {})
if not user_attrs:
response.status = 400 # Bad request
return {"reason": "user_attrs must be specified."}
try:
for key, val in user_attrs.items():
storage.set_trial_user_attr(trial_id, key, val)
except Exception as e:
response.status = 500
return {"reason": f"Internal server error: {e}"}
response.status = 204
return {}
@app.put("/api/studies/<study_id:int>/<trial_id:int>/note")
@json_api_view
def save_trial_note(study_id: int, trial_id: int) -> dict[str, Any]:
+183
View File
@@ -0,0 +1,183 @@
from __future__ import annotations
from dataclasses import dataclass
import json
from typing import TYPE_CHECKING
from typing import Union
import warnings
import optuna
if TYPE_CHECKING:
from typing import Any
from typing import Literal
from typing import Optional
from typing import TypedDict
ChoiceWidgetJSON = TypedDict(
"ChoiceWidgetJSON",
{
"type": Literal["choice"],
"description": Optional[str],
"choices": list[str],
"values": list[float],
"user_attr_key": Optional[str],
},
)
SliderWidgetLabel = TypedDict(
"SliderWidgetLabel",
{"value": float, "label": str},
)
SliderWidgetJSON = TypedDict(
"SliderWidgetJSON",
{
"type": Literal["slider"],
"description": Optional[str],
"min": float,
"max": float,
"step": Optional[float],
"labels": Optional[list[SliderWidgetLabel]],
"user_attr_key": Optional[str],
},
)
TextInputWidgetJSON = TypedDict(
"TextInputWidgetJSON",
{"type": Literal["text"], "description": Optional[str], "user_attr_key": Optional[str]},
)
UserAttrRefJSON = TypedDict(
"UserAttrRefJSON",
{"type": Literal["user_attr"], "key": str, "user_attr_key": Optional[str]},
)
FormWidgetJSON = TypedDict(
"FormWidgetJSON",
{
"output_type": Literal["objective", "user_attr"],
"widgets": list[
Union[ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON, UserAttrRefJSON]
],
},
)
@dataclass
class ChoiceWidget:
choices: list[str]
values: list[float]
description: Optional[str] = None
user_attr_key: Optional[str] = None
def to_dict(self) -> ChoiceWidgetJSON:
return {
"type": "choice",
"description": self.description,
"choices": self.choices,
"values": self.values,
"user_attr_key": self.user_attr_key,
}
@dataclass
class SliderWidget:
min: float
max: float
step: Optional[float] = None
labels: Optional[list[tuple[float, str]]] = None
description: Optional[str] = None
user_attr_key: Optional[str] = None
def to_dict(self) -> SliderWidgetJSON:
labels: Optional[list[SliderWidgetLabel]] = None
if self.labels is not None:
labels = [{"value": value, "label": label} for value, label in self.labels]
return {
"type": "slider",
"description": self.description,
"min": self.min,
"max": self.max,
"step": self.step,
"labels": labels,
"user_attr_key": self.user_attr_key,
}
@dataclass
class TextInputWidget:
description: Optional[str] = None
user_attr_key: Optional[str] = None
def to_dict(self) -> TextInputWidgetJSON:
return {
"type": "text",
"description": self.description,
"user_attr_key": self.user_attr_key,
}
@dataclass
class ObjectiveUserAttrRef:
key: str
user_attr_key: Optional[str] = None
def to_dict(self) -> UserAttrRefJSON:
return {
"type": "user_attr",
"key": self.key,
"user_attr_key": self.user_attr_key,
}
ObjectiveFormWidget = Union[ChoiceWidget, SliderWidget, TextInputWidget, ObjectiveUserAttrRef]
# For backward compatibility.
ObjectiveChoiceWidget = ChoiceWidget
ObjectiveSliderWidget = SliderWidget
ObjectiveTextInputWidget = TextInputWidget
FORM_WIDGETS_KEY = "dashboard:form_widgets:v2"
def register_objective_form_widgets(
study: optuna.Study, widgets: list[ObjectiveFormWidget]
) -> None:
if len(study.directions) != len(widgets):
raise ValueError("The length of actions must be the same with the number of objectives.")
if any(w.user_attr_key is not None for w in widgets):
warnings.warn("`user_attr_key` specified, but it will not be used.")
form_widgets: FormWidgetJSON = {
"output_type": "objective",
"widgets": [w.to_dict() for w in widgets],
}
study._storage.set_study_system_attr(study._study_id, FORM_WIDGETS_KEY, form_widgets)
def register_user_attr_form_widgets(
study: optuna.Study, widgets: list[ObjectiveFormWidget]
) -> None:
if any(w.user_attr_key is None for w in widgets):
raise ValueError("`user_attr_key` is not specified.")
if len(widgets) != len(set(w.user_attr_key for w in widgets)):
raise ValueError("`user_attr_key` must be unique for each widget.")
form_widgets: FormWidgetJSON = {
"output_type": "user_attr",
"widgets": [w.to_dict() for w in widgets],
}
study._storage.set_study_system_attr(study._study_id, FORM_WIDGETS_KEY, form_widgets)
def get_form_widgets_json(study_system_attr: dict[str, Any]) -> Optional[FormWidgetJSON]:
if FORM_WIDGETS_KEY in study_system_attr:
return study_system_attr[FORM_WIDGETS_KEY]
# For optuna-dashboard v0.9.0 and v0.9.0b6 users
if "dashboard:objective_form_widgets:v1" in study_system_attr:
return {
"output_type": "objective",
"widgets": study_system_attr["dashboard:objective_form_widgets:v1"],
}
# For optuna-dashboard v0.9.0b5 users
if "dashboard:objective_form_widgets" in study_system_attr:
return {
"output_type": "objective",
"widgets": json.loads(study_system_attr["dashboard:objective_form_widgets"]),
}
return None
-133
View File
@@ -1,133 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
import json
from typing import TYPE_CHECKING
from typing import Union
import optuna
if TYPE_CHECKING:
from typing import Any
from typing import Literal
from typing import Optional
from typing import TypedDict
ChoiceWidgetJSON = TypedDict(
"ChoiceWidgetJSON",
{
"type": Literal["choice"],
"description": Optional[str],
"choices": list[str],
"values": list[float],
},
)
SliderWidgetLabel = TypedDict(
"SliderWidgetLabel",
{"value": float, "label": str},
)
SliderWidgetJSON = TypedDict(
"SliderWidgetJSON",
{
"type": Literal["slider"],
"description": Optional[str],
"min": float,
"max": float,
"step": Optional[float],
"labels": Optional[list[SliderWidgetLabel]],
},
)
TextInputWidgetJSON = TypedDict(
"TextInputWidgetJSON",
{"type": Literal["text"], "description": Optional[str]},
)
UserAttrRefJSON = TypedDict("UserAttrRefJSON", {"type": Literal["user_attr"], "key": str})
ObjectiveFormWidgetJSON = Union[
ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON, UserAttrRefJSON
]
@dataclass
class ObjectiveChoiceWidget:
choices: list[str]
values: list[float]
description: Optional[str] = None
def to_dict(self) -> ChoiceWidgetJSON:
return {
"type": "choice",
"description": self.description,
"choices": self.choices,
"values": self.values,
}
@dataclass
class ObjectiveSliderWidget:
min: float
max: float
step: Optional[float] = None
labels: Optional[list[tuple[float, str]]] = None
description: Optional[str] = None
def to_dict(self) -> SliderWidgetJSON:
labels: Optional[list[SliderWidgetLabel]] = None
if self.labels is not None:
labels = [{"value": value, "label": label} for value, label in self.labels]
return {
"type": "slider",
"description": self.description,
"min": self.min,
"max": self.max,
"step": self.step,
"labels": labels,
}
@dataclass
class ObjectiveTextInputWidget:
description: Optional[str] = None
def to_dict(self) -> TextInputWidgetJSON:
return {
"type": "text",
"description": self.description,
}
@dataclass
class ObjectiveUserAttrRef:
key: str
def to_dict(self) -> UserAttrRefJSON:
return {
"type": "user_attr",
"key": self.key,
}
ObjectiveFormWidget = Union[
ObjectiveChoiceWidget, ObjectiveSliderWidget, ObjectiveTextInputWidget, ObjectiveUserAttrRef
]
SYSTEM_ATTR_KEY = "dashboard:objective_form_widgets:v1"
def register_objective_form_widgets(
study: optuna.Study, widgets: list[ObjectiveFormWidget]
) -> None:
if len(study.directions) != len(widgets):
raise ValueError("The length of actions must be the same with the number of objectives.")
widget_dicts = [w.to_dict() for w in widgets]
study._storage.set_study_system_attr(study._study_id, SYSTEM_ATTR_KEY, widget_dicts)
def get_objective_form_widgets_json(
study_system_attr: dict[str, Any]
) -> Optional[list[ObjectiveFormWidgetJSON]]:
if SYSTEM_ATTR_KEY in study_system_attr:
return study_system_attr[SYSTEM_ATTR_KEY]
# For optuna-dashboard v0.9.0b5 users
if "dashboard:objective_form_widgets" in study_system_attr:
return json.loads(study_system_attr["dashboard:objective_form_widgets"])
return None
+4 -4
View File
@@ -12,8 +12,8 @@ from optuna.study import StudySummary
from optuna.trial import FrozenTrial
from . import _note as note
from ._form_widget import get_form_widgets_json
from ._named_objectives import get_objective_names
from ._objective_form_widget import get_objective_form_widgets_json
from .artifact._backend import list_trial_artifacts
@@ -144,9 +144,9 @@ def serialize_study_detail(
objective_names = get_objective_names(system_attrs)
if objective_names:
serialized["objective_names"] = objective_names
objective_form_widgets = get_objective_form_widgets_json(system_attrs)
if objective_form_widgets:
serialized["objective_form_widgets"] = objective_form_widgets
form_widgets = get_form_widgets_json(system_attrs)
if form_widgets:
serialized["form_widgets"] = form_widgets
return serialized
+56
View File
@@ -9,6 +9,7 @@ import {
saveStudyNoteAPI,
saveTrialNoteAPI,
tellTrialAPI,
saveTrialUserAttrsAPI,
renameStudyAPI,
uploadArtifactAPI,
getMetaInfoAPI,
@@ -151,6 +152,26 @@ export const actionCreator = () => {
setStudyDetailState(studyId, newStudy)
}
const setTrialUserAttrs = (
studyId: number,
index: number,
user_attrs: { [key: string]: number }
) => {
const newTrial: Trial = Object.assign(
{},
studyDetails[studyId].trials[index]
)
newTrial.user_attrs = Object.keys(user_attrs).map((key) => ({
key: key,
value: user_attrs[key].toString(),
}))
const newTrials: Trial[] = [...studyDetails[studyId].trials]
newTrials[index] = newTrial
const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId])
newStudy.trials = newTrials
setStudyDetailState(studyId, newStudy)
}
const setStudyParamImportanceState = (
studyId: number,
importance: ParamImportance[][]
@@ -505,6 +526,40 @@ export const actionCreator = () => {
})
}
const saveTrialUserAttrs = (
studyId: number,
trialId: number,
user_attrs: { [key: string]: number }
): void => {
console.log("user_attrs", user_attrs)
const message = `id=${trialId}, user_attrs=${JSON.stringify(user_attrs)}`
saveTrialUserAttrsAPI(trialId, user_attrs)
.then(() => {
const index = studyDetails[studyId].trials.findIndex(
(t) => t.trial_id === trialId
)
if (index === -1) {
enqueueSnackbar(`Unexpected error happens. Please reload the page.`, {
variant: "error",
})
return
}
setTrialUserAttrs(studyId, index, user_attrs)
enqueueSnackbar(`Successfully updated trial (${message})`, {
variant: "success",
})
})
.catch((err) => {
const reason = err.response?.data.reason
enqueueSnackbar(
`Failed to update trial (${message}). Reason: ${reason}`,
{
variant: "error",
}
)
console.log(err)
})
}
return {
updateAPIMeta,
updateStudyDetail,
@@ -523,6 +578,7 @@ export const actionCreator = () => {
deleteArtifact,
makeTrialComplete,
makeTrialFail,
saveTrialUserAttrs,
}
}
+15 -2
View File
@@ -67,7 +67,7 @@ interface StudyDetailResponse {
has_intermediate_values: boolean
note: Note
objective_names?: string[]
objective_form_widgets?: ObjectiveFormWidget[]
form_widgets?: FormWidgets
}
export const getStudyDetailAPI = (
@@ -100,7 +100,7 @@ export const getStudyDetailAPI = (
has_intermediate_values: res.data.has_intermediate_values,
note: res.data.note,
objective_names: res.data.objective_names,
objective_form_widgets: res.data.objective_form_widgets,
form_widgets: res.data.form_widgets,
}
})
}
@@ -281,6 +281,19 @@ export const tellTrialAPI = (
})
}
export const saveTrialUserAttrsAPI = (
trialId: number,
user_attrs: { [key: string]: number }
): Promise<void> => {
const req = { user_attrs: user_attrs }
return axiosInstance
.post<void>(`/api/trials/${trialId}/user-attrs`, req)
.then((res) => {
return
})
}
interface ParamImportancesResponse {
param_importances: ParamImportance[][]
}
@@ -20,13 +20,12 @@ export const ObjectiveForm: FC<{
trial: Trial
directions: StudyDirection[]
names: string[]
widgets: ObjectiveFormWidget[]
}> = ({ trial, directions, names, widgets }) => {
formWidgets: FormWidgets
}> = ({ trial, directions, names, formWidgets }) => {
const theme = useTheme()
const action = actionCreator()
const [values, setValues] = useState<(number | null)[]>(
directions.map((d, i) => {
const widget = widgets.at(i)
formWidgets.widgets.map((widget) => {
if (widget === undefined) {
return null
} else if (widget.type === "text") {
@@ -65,23 +64,38 @@ export const ObjectiveForm: FC<{
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>): void => {
e.preventDefault()
const filtered = values.filter<number>((v): v is number => v !== null)
if (filtered.length !== directions.length) {
return
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)
}
action.makeTrialComplete(trial.study_id, trial.trial_id, filtered)
}
const getObjectiveName = (i: number): string => {
const n = names.at(i)
if (n !== undefined) {
return n
}
if (directions.length == 1) {
return `Objective`
} else {
return `Objective ${i}`
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 (
@@ -102,44 +116,14 @@ export const ObjectiveForm: FC<{
p: theme.spacing(1),
}}
>
{directions.map((d, i) => {
const widget = widgets.at(i)
{formWidgets.widgets.map((widget, i) => {
const value = values.at(i)
const key = `objective-${i}`
if (widget === undefined) {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>{getObjectiveName(i)}</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.`
: "",
label: getObjectiveName(i),
type: "text",
}}
/>
</FormControl>
)
} else if (widget.type === "text") {
if (widget.type === "text") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getObjectiveName(i)} - {widget.description}
{getMetricName(i)} - {widget.description}
</FormLabel>
<DebouncedInputTextField
onChange={(s, valid) => {
@@ -172,7 +156,7 @@ export const ObjectiveForm: FC<{
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getObjectiveName(i)} - {widget.description}
{getMetricName(i)} - {widget.description}
</FormLabel>
<RadioGroup row defaultValue={widget.values.at(0)}>
{widget.choices.map((c, j) => (
@@ -202,7 +186,7 @@ export const ObjectiveForm: FC<{
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getObjectiveName(i)} - {widget.description}
{getMetricName(i)} - {widget.description}
</FormLabel>
<Box sx={{ padding: theme.spacing(0, 2) }}>
<Slider
@@ -227,7 +211,7 @@ export const ObjectiveForm: FC<{
} else if (widget.type === "user_attr") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>{getObjectiveName(i)}</FormLabel>
<FormLabel>{getMetricName(i)}</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={value || undefined}
@@ -280,19 +264,24 @@ export const ReadonlyObjectiveForm: FC<{
trial: Trial
directions: StudyDirection[]
names: string[]
widgets: ObjectiveFormWidget[]
}> = ({ trial, directions, names, widgets }) => {
formWidgets: FormWidgets
}> = ({ trial, directions, names, formWidgets }) => {
const theme = useTheme()
const getObjectiveName = (i: number): string => {
const n = names.at(i)
if (n !== undefined) {
return n
}
if (directions.length == 1) {
return `Objective`
} else {
return `Objective ${i}`
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 (
<>
@@ -312,24 +301,13 @@ export const ReadonlyObjectiveForm: FC<{
p: theme.spacing(1),
}}
>
{directions.map((d, i) => {
const widget = widgets.at(i)
{formWidgets.widgets.map((widget, i) => {
const key = `objective-${i}`
if (widget === undefined) {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>{getObjectiveName(i)}</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={trial.values?.at(i)}
/>
</FormControl>
)
} else if (widget.type === "text") {
if (widget.type === "text") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getObjectiveName(i)} - {widget.description}
{getMetricName(i)} - {widget.description}
</FormLabel>
<TextField
inputProps={{ readOnly: true }}
@@ -341,7 +319,7 @@ export const ReadonlyObjectiveForm: FC<{
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getObjectiveName(i)} - {widget.description}
{getMetricName(i)} - {widget.description}
</FormLabel>
<RadioGroup row defaultValue={trial.values?.at(i)}>
{widget.choices.map((c, j) => (
@@ -366,7 +344,7 @@ export const ReadonlyObjectiveForm: FC<{
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>
{getObjectiveName(i)} - {widget.description}
{getMetricName(i)} - {widget.description}
</FormLabel>
<Box sx={{ padding: theme.spacing(0, 2) }}>
<Slider
@@ -390,7 +368,7 @@ export const ReadonlyObjectiveForm: FC<{
} else if (widget.type === "user_attr") {
return (
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
<FormLabel>{getObjectiveName(i)}</FormLabel>
<FormLabel>{getMetricName(i)}</FormLabel>
<TextField
inputProps={{ readOnly: true }}
value={trial.values?.at(i)}
+23 -27
View File
@@ -143,14 +143,8 @@ const TrialListDetail: FC<{
isBestTrial: (trialId: number) => boolean
directions: StudyDirection[]
objectiveNames: string[]
objectiveFormWidgets: ObjectiveFormWidget[]
}> = ({
trial,
isBestTrial,
directions,
objectiveNames,
objectiveFormWidgets,
}) => {
formWidgets?: FormWidgets
}> = ({ trial, isBestTrial, directions, objectiveNames, formWidgets }) => {
const theme = useTheme()
const artifactEnabled = useRecoilValue<boolean>(artifactIsAvailable)
const startMs = trial.datetime_start?.getTime()
@@ -291,22 +285,26 @@ const TrialListDetail: FC<{
latestNote={trial.note}
cardSx={{ marginBottom: theme.spacing(2) }}
/>
{trial.state === "Running" && directions.length > 0 && (
<ObjectiveForm
trial={trial}
directions={directions}
names={objectiveNames}
widgets={objectiveFormWidgets}
/>
)}
{trial.state === "Complete" && directions.length > 0 && (
<ReadonlyObjectiveForm
trial={trial}
directions={directions}
names={objectiveNames}
widgets={objectiveFormWidgets}
/>
)}
{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}
/>
)}
{artifactEnabled && <TrialArtifact trial={trial} />}
</Box>
)
@@ -813,9 +811,7 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
isBestTrial={isBestTrial}
directions={studyDetail?.directions || []}
objectiveNames={studyDetail?.objective_names || []}
objectiveFormWidgets={
studyDetail?.objective_form_widgets || []
}
formWidgets={studyDetail?.form_widgets}
/>
))}
</Box>
+11 -1
View File
@@ -128,6 +128,7 @@ type StudySummary = {
type ObjectiveChoiceWidget = {
type: "choice"
description: string
user_attr_key?: string
choices: string[]
values: number[]
}
@@ -135,6 +136,7 @@ type ObjectiveChoiceWidget = {
type ObjectiveSliderWidget = {
type: "slider"
description: string
user_attr_key?: string
min: number
max: number
step: number
@@ -149,19 +151,27 @@ type ObjectiveSliderWidget = {
type ObjectiveTextInputWidget = {
type: "text"
description: string
user_attr_key?: string
}
type ObjectiveUserAttrRef = {
type: "user_attr"
key: string
user_attr_key?: string
}
// TODO(kenshin): Rename this type to FormWidget or something.
type ObjectiveFormWidget =
| ObjectiveChoiceWidget
| ObjectiveSliderWidget
| ObjectiveTextInputWidget
| ObjectiveUserAttrRef
type FormWidgets = {
output_type: "objective" | "user_attr"
widgets: ObjectiveFormWidget[]
}
type StudyDetail = {
id: number
name: string
@@ -175,7 +185,7 @@ type StudyDetail = {
has_intermediate_values: boolean
note: Note
objective_names?: string[]
objective_form_widgets?: ObjectiveFormWidget[]
form_widgets?: FormWidgets
}
type StudyDetails = {