ivoryos 1.3.8__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

Files changed (34) hide show
  1. ivoryos/optimizer/ax_optimizer.py +44 -25
  2. ivoryos/optimizer/base_optimizer.py +15 -1
  3. ivoryos/optimizer/baybe_optimizer.py +24 -17
  4. ivoryos/optimizer/nimo_optimizer.py +26 -24
  5. ivoryos/routes/data/data.py +27 -9
  6. ivoryos/routes/data/templates/components/step_card.html +47 -11
  7. ivoryos/routes/data/templates/workflow_view.html +14 -5
  8. ivoryos/routes/design/design.py +31 -1
  9. ivoryos/routes/design/design_step.py +2 -1
  10. ivoryos/routes/design/templates/components/edit_action_form.html +16 -3
  11. ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
  12. ivoryos/routes/design/templates/experiment_builder.html +1 -0
  13. ivoryos/routes/execute/execute.py +36 -7
  14. ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
  15. ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
  16. ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
  17. ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
  18. ivoryos/routes/execute/templates/experiment_run.html +0 -264
  19. ivoryos/socket_handlers.py +1 -1
  20. ivoryos/static/js/action_handlers.js +252 -118
  21. ivoryos/static/js/sortable_design.js +1 -0
  22. ivoryos/utils/bo_campaign.py +17 -16
  23. ivoryos/utils/db_models.py +122 -18
  24. ivoryos/utils/decorators.py +1 -0
  25. ivoryos/utils/form.py +22 -5
  26. ivoryos/utils/nest_script.py +314 -0
  27. ivoryos/utils/script_runner.py +447 -147
  28. ivoryos/utils/utils.py +11 -1
  29. ivoryos/version.py +1 -1
  30. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/METADATA +3 -2
  31. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/RECORD +34 -33
  32. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/WHEEL +0 -0
  33. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/licenses/LICENSE +0 -0
  34. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/top_level.txt +0 -0
@@ -2,397 +2,519 @@
2
2
 
3
3
  <div class="tab-pane fade" id="tab3" role="tabpanel" aria-labelledby="tab3-tab">
4
4
 
5
+ <h6 class="fw-bold mt-2 mb-1">Load Previous Data</h6>
6
+
7
+ <form method="POST" name="bo" action="{{ url_for('execute.run_bo') }}">
8
+ <div class="container py-2">
9
+
10
+ <!-- Data Loading Section -->
11
+ <div class="input-group mb-3">
12
+ <label class="input-group-text"><i class="bi bi-folder2-open"></i></label>
13
+ <select class="form-select" id="existing_data" name="existing_data">
14
+ <option value="">Load existing data...</option>
15
+ {% for data in data_list %}
16
+ <option value="{{ data }}">{{ data }} </option>
17
+ {% endfor %}
18
+ </select>
19
+ </div>
5
20
 
6
- {# <div class="row align-items-center mb-3">#}
7
- {# <div class="col-6">#}
8
- {# <form method="POST" id="loadHistory" name="loadHistory" action="{{ url_for('execute.execute_files.upload_history') }}" enctype="multipart/form-data">#}
9
- {# <div class="input-group">#}
10
- {# <input class="form-control" name="historyfile" id="historyfile" type="file" accept=".csv" onchange="var f=document.getElementById('loadHistory'); if(f) f.submit();"> </div>#}
11
- {# </form>#}
12
- {# </div>#}
13
- {# </div>#}
14
- <h6 class="fw-bold mt-2 mb-1">Load Previous Data</h6>
15
-
16
- <form method="POST" name="bo" action="{{ url_for('execute.run_bo') }}">
17
- <div class="container py-2">
18
-
19
- <!-- Data Loading Section -->
20
- <div class="input-group mb-3">
21
- <label class="input-group-text"><i class="bi bi-folder2-open"></i></label>
22
- <select class="form-select" id="existing_data" name="existing_data">
23
- <option value="">Load existing data...</option>
24
- {% for data in data_list %}
25
- <option value="{{ data }}">{{ data }} </option>
26
- {% endfor %}
27
- </select>
28
- </div>
29
-
30
- <!-- Data preview section -->
31
- <div class="row mb-3" id="data_preview_section" style="display: none;">
32
- <div class="col-12">
33
- <div class="card">
34
- <div class="card-header py-2">
35
- <small class="fw-bold">Data Preview</small>
36
- </div>
37
- <div class="card-body py-2">
38
- <div id="data_preview_content">
39
- <small class="text-muted">Select a data source to preview</small>
40
- </div>
21
+ <!-- Data preview section -->
22
+ <div class="row mb-3" id="data_preview_section" style="display: none;">
23
+ <div class="col-12">
24
+ <div class="card">
25
+ <div class="card-header py-2">
26
+ <small class="fw-bold">Data Preview</small>
27
+ </div>
28
+ <div class="card-body py-2">
29
+ <div id="data_preview_content">
30
+ <small class="text-muted">Select a data source to preview</small>
41
31
  </div>
42
32
  </div>
43
33
  </div>
44
34
  </div>
35
+ </div>
45
36
 
46
- <hr class="my-3">
47
- <!-- Optimizer Selection -->
48
- <div class="input-group mb-3">
49
- <label class="input-group-text"><i class="bi bi-gear"></i></label>
50
- <select class="form-select" id="optimizer_type" name="optimizer_type" onchange="updateOptimizerInfo()">
51
- <option value="">Select optimizer...</option>
52
- {% for optimizer_name, optimizer_info in optimizer_schema.items() %}
53
- <option value="{{ optimizer_name }}"
54
- data-multiobjective="{{ optimizer_info.multiple_objectives }}"
55
- data-parameter-types="{{ optimizer_info.parameter_types|join(',') }}"
56
- data-optimizer-config="{{ optimizer_info.optimizer_config|tojson|e }}">
57
- {{ optimizer_name }} {% if optimizer_info.multiple_objectives %}(Multi-objective supported){% endif %}
58
- </option>
59
- {% endfor %}
60
- </select>
61
- </div>
62
- <!-- Tabs Navigation -->
63
- <ul class="nav nav-tabs" id="configTabs" role="tablist">
64
- <li class="nav-item" role="presentation">
65
- <button class="nav-link active" id="parameters-tab" data-bs-toggle="tab" data-bs-target="#parameters" type="button" role="tab" aria-controls="parameters" aria-selected="true">
66
- Parameters
67
- </button>
68
- </li>
69
- <li class="nav-item" role="presentation">
70
- <button class="nav-link" id="advanced-tab" data-bs-toggle="tab" data-bs-target="#advanced" type="button" role="tab" aria-controls="advanced" aria-selected="false">
71
- Advanced Settings
72
- </button>
73
- </li>
74
- </ul>
75
-
76
- <!-- Tab Content -->
77
- <div class="tab-content" id="configTabContent">
78
- <!-- Parameters Tab -->
79
- <div class="tab-pane fade show active" id="parameters" role="tabpanel" aria-labelledby="parameters-tab">
80
- <div class="py-3">
81
- <h6 class="fw-bold mt-2 mb-1">Parameters</h6>
82
- <div id="parameters_container">
83
- {% for config in config_list %}
84
- <div class="row align-items-center mb-2 parameter-row">
85
- <div class="col-3 col-form-label-sm">
86
- {{ config }}:
87
- </div>
88
- <div class="col-3">
89
- <select class="form-select form-select-sm parameter-type" id="{{config}}_type" name="{{config}}_type" onchange="updateParameterInputs(this)">
90
- <!-- Options will be populated by JavaScript -->
91
- </select>
92
- </div>
93
- <div class="col-6 parameter-inputs">
94
- <input type="text" class="form-control form-control-sm single-input" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">
95
- <div class="range-inputs" style="display: none;">
96
- <div class="row">
97
- <div class="col-6">
98
- <input type="text" class="form-control form-control-sm" id="{{config}}_min" name="{{config}}_min" placeholder="Min value">
99
- </div>
100
- <div class="col-6">
101
- <input type="text" class="form-control form-control-sm" id="{{config}}_max" name="{{config}}_max" placeholder="Max value">
102
- </div>
37
+ <hr class="my-3">
38
+ <!-- Optimizer Selection -->
39
+ <div class="input-group mb-3">
40
+ <label class="input-group-text"><i class="bi bi-gear"></i></label>
41
+ <select class="form-select" id="optimizer_type" name="optimizer_type" >
42
+ <option value="">Select optimizer...</option>
43
+ {% for optimizer_name, optimizer_info in optimizer_schema.items() %}
44
+ <option value="{{ optimizer_name }}"
45
+ data-multiobjective="{{ optimizer_info.multiple_objectives }}"
46
+ data-parameter-types="{{ optimizer_info.parameter_types|join(',') }}"
47
+ data-optimizer-config="{{ optimizer_info.optimizer_config|tojson|e }}">
48
+ {{ optimizer_name }} {% if optimizer_info.multiple_objectives %}(Multi-objective supported){% endif %}
49
+ </option>
50
+ {% endfor %}
51
+ </select>
52
+ </div>
53
+ <!-- Tabs Navigation -->
54
+ <ul class="nav nav-tabs" id="configTabs" role="tablist">
55
+ <li class="nav-item" role="presentation">
56
+ <button class="nav-link active" id="parameters-tab" data-bs-toggle="tab" data-bs-target="#parameters" type="button" role="tab" aria-controls="parameters" aria-selected="true">
57
+ Parameters
58
+ </button>
59
+ </li>
60
+ <li class="nav-item" role="presentation">
61
+ <button class="nav-link" id="advanced-tab" data-bs-toggle="tab" data-bs-target="#advanced" type="button" role="tab" aria-controls="advanced" aria-selected="false">
62
+ Advanced Settings
63
+ </button>
64
+ </li>
65
+ </ul>
66
+
67
+ <!-- Tab Content -->
68
+ <div class="tab-content" id="configTabContent">
69
+ <!-- Parameters Tab -->
70
+ <div class="tab-pane fade show active" id="parameters" role="tabpanel" aria-labelledby="parameters-tab">
71
+ <div class="py-3">
72
+
73
+ <!-- Parameters -->
74
+ <h6 class="fw-bold mt-2 mb-1">Parameters</h6>
75
+ <div id="parameters_container">
76
+ {% for config in config_list %}
77
+ <div class="row align-items-center mb-2 parameter-row">
78
+ <div class="col-3 col-form-label-sm">
79
+ {{ config }}:
80
+ </div>
81
+ <div class="col-3">
82
+ <select class="form-select form-select-sm parameter-type" id="{{config}}_type" name="{{config}}_type" onchange="updateParameterInputs(this)">
83
+ <!-- Options will be populated by JavaScript -->
84
+ </select>
85
+ </div>
86
+ <div class="col-6 parameter-inputs">
87
+ <input type="text" class="form-control form-control-sm single-input" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">
88
+ <div class="range-inputs" style="display: none;">
89
+ <div class="row g-2">
90
+ <div class="col-4">
91
+ <input type="text" class="form-control form-control-sm" id="{{config}}_min" name="{{config}}_min" placeholder="Min" required>
92
+ </div>
93
+ <div class="col-4">
94
+ <input type="text" class="form-control form-control-sm" id="{{config}}_max" name="{{config}}_max" placeholder="Max" required>
95
+ </div>
96
+ <div class="col-4">
97
+ <input type="text" class="form-control form-control-sm" id="{{config}}_step" name="{{config}}_step" placeholder="Step (optional)">
103
98
  </div>
104
99
  </div>
105
100
  </div>
106
101
  </div>
107
- {% endfor %}
102
+ </div>
103
+ {% endfor %}
104
+ </div>
105
+
106
+ <!-- Objectives -->
107
+ <h6 class="fw-bold mt-3 mb-1">Objectives</h6>
108
+ {% for objective in return_list %}
109
+ <div class="row align-items-center mb-2">
110
+ <div class="col-3 col-form-label-sm">
111
+ {{ objective }}:
112
+ </div>
113
+ <div class="col-6">
114
+ <select class="form-select form-select-sm" id="{{objective}}_obj_min" name="{{objective}}_obj_min">
115
+ <option selected>minimize</option>
116
+ <option>maximize</option>
117
+ <option>none</option>
118
+ </select>
119
+ </div>
120
+ <div class="col-3">
121
+ <input class="form-control" type="number" step="any" id="{{objective}}_obj_threshold" name="{{objective}}_obj_threshold" placeholder="Early Stop (optional)">
122
+ </div>
108
123
  </div>
124
+ {% endfor %}
109
125
 
110
- <!-- Objectives -->
111
- <h6 class="fw-bold mt-3 mb-1">Objectives</h6>
112
- {% for objective in return_list %}
113
- <div class="row align-items-center mb-2">
114
- <div class="col-3 col-form-label-sm">
115
- {{ objective }}:
116
- </div>
117
- <div class="col-6">
118
- <select class="form-select form-select-sm" id="{{objective}}_min" name="{{objective}}_min">
119
- <option selected>minimize</option>
120
- <option>maximize</option>
121
- <option>none</option>
122
- </select>
126
+ <!-- Constraints -->
127
+ <div id="constraints_section">
128
+ <h6 class="fw-bold mt-3 mb-1">Constraints</h6>
129
+ <div id="constraints_container">
130
+ <div class="row align-items-center mb-2 constraint-row">
131
+ <div class="col-10">
132
+ <input type="text" class="form-control form-control-sm" name="constraint_expr" placeholder="e.g. a + b <= 80">
123
133
  </div>
124
- {% if not return_list|length == 1 %}
125
- <div class="col-3">
126
- <input class="form-control" type="number" id="{{objective}}_weight" name="{{objective}}_weight" min="1" max="1000" value="1">
134
+ <div class="col-2">
135
+ <button type="button" class="btn btn-outline-secondary btn-sm" onclick="addConstraintRow()">+</button>
127
136
  </div>
128
- {% endif %}
129
137
  </div>
130
- {% endfor %}
131
-
132
- <!-- Budget -->
133
- <h6 class="fw-bold mt-3 mb-1">Budget</h6>
134
- <div class="input-group mb-3">
135
- <label class="input-group-text" for="repeat">Max iteration </label>
136
- <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">
137
138
  </div>
138
139
  </div>
140
+
141
+ <!-- Budget -->
142
+ <h6 class="fw-bold mt-3 mb-1">Budget</h6>
143
+ <div class="input-group mb-3">
144
+ <label class="input-group-text" for="batch_size"># of suggestions </label>
145
+ <input class="form-control" type="number" id="batch_size" name="batch_size" min="1" max="100" value="1">
146
+ <label class="input-group-text" for="repeat">Max iteration </label>
147
+ <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">
148
+ </div>
139
149
  </div>
150
+ </div>
140
151
 
141
- <!-- Advanced Settings Tab -->
142
- <div class="tab-pane fade" id="advanced" role="tabpanel" aria-labelledby="advanced-tab">
143
- <div class="py-3">
144
- <h6 class="fw-bold mt-2 mb-1">Optimizer Configuration</h6>
145
- <div id="optimizer_config_container" style="display: none;">
152
+ <!-- Advanced Settings Tab -->
153
+ <div class="tab-pane fade" id="advanced" role="tabpanel" aria-labelledby="advanced-tab">
154
+ <div class="py-3">
155
+ <h6 class="fw-bold mt-2 mb-1">Optimizer Configuration</h6>
156
+ <div id="optimizer_config_container" style="display: none;">
146
157
 
147
- <!-- Step 1 Configuration -->
148
- <div class="card mb-3">
149
- <div class="card-header py-2">
150
- <small class="fw-bold">Step 1 Configuration</small>
158
+ <!-- Step 1 Configuration -->
159
+ <div class="card mb-3">
160
+ <div class="card-header py-2">
161
+ <small class="fw-bold">Step 1 Configuration</small>
162
+ </div>
163
+ <div class="card-body py-2">
164
+ <div class="row align-items-center mb-2">
165
+ <div class="col-3 col-form-label-sm">
166
+ Model:
167
+ </div>
168
+ <div class="col-6">
169
+ <select class="form-select form-select-sm" id="step1_model" name="step1_model">
170
+ <!-- Options will be populated by JavaScript -->
171
+ </select>
172
+ </div>
151
173
  </div>
152
- <div class="card-body py-2">
153
- <div class="row align-items-center mb-2">
154
- <div class="col-3 col-form-label-sm">
155
- Model:
156
- </div>
157
- <div class="col-6">
158
- <select class="form-select form-select-sm" id="step1_model" name="step1_model">
159
- <!-- Options will be populated by JavaScript -->
160
- </select>
161
- </div>
174
+ <div class="row align-items-center mb-2" id="step1_num_samples_row">
175
+ <div class="col-3 col-form-label-sm">
176
+ Num Samples:
162
177
  </div>
163
- <div class="row align-items-center mb-2" id="step1_num_samples_row">
164
- <div class="col-3 col-form-label-sm">
165
- Num Samples:
166
- </div>
167
- <div class="col-6">
168
- <input type="number" class="form-control form-control-sm" id="step1_num_samples" name="step1_num_samples" min="1" value="">
169
- </div>
178
+ <div class="col-6">
179
+ <input type="number" class="form-control form-control-sm" id="step1_num_samples" name="step1_num_samples" min="0" value="">
170
180
  </div>
171
181
  </div>
172
182
  </div>
183
+ </div>
173
184
 
174
- <!-- Step 2 Configuration -->
175
- <div class="card mb-3">
176
- <div class="card-header py-2">
177
- <small class="fw-bold">Step 2 Configuration</small>
178
- </div>
179
- <div class="card-body py-2">
180
- <div class="row align-items-center mb-2">
181
- <div class="col-3 col-form-label-sm">
182
- Model:
183
- </div>
184
- <div class="col-6">
185
- <select class="form-select form-select-sm" id="step2_model" name="step2_model">
186
- <!-- Options will be populated by JavaScript -->
187
- </select>
188
- </div>
185
+ <!-- Step 2 Configuration -->
186
+ <div class="card mb-3">
187
+ <div class="card-header py-2">
188
+ <small class="fw-bold">Step 2 Configuration</small>
189
+ </div>
190
+ <div class="card-body py-2">
191
+ <div class="row align-items-center mb-2">
192
+ <div class="col-3 col-form-label-sm">
193
+ Model:
194
+ </div>
195
+ <div class="col-6">
196
+ <select class="form-select form-select-sm" id="step2_model" name="step2_model">
197
+ <!-- Options will be populated by JavaScript -->
198
+ </select>
189
199
  </div>
190
200
  </div>
191
201
  </div>
192
202
  </div>
193
- <div id="optimizer_config_placeholder">
194
- <small class="text-muted">Select an optimizer to configure advanced settings</small>
195
- </div>
203
+ </div>
204
+ <div id="optimizer_config_placeholder">
205
+ <small class="text-muted">Select an optimizer to configure advanced settings</small>
196
206
  </div>
197
207
  </div>
198
208
  </div>
199
-
200
- {% if not no_deck_warning%}
201
- <div class="input-group mb-3 mt-3">
202
- <button class="form-control" type="submit" name="bo">Run</button>
203
- </div>
204
- {% endif %}
205
209
  </div>
206
- </form>
207
- </div>
208
- <script>
209
-
210
- const optimizerSchema = {{ optimizer_schema|tojson }};
211
-
212
- document.addEventListener('DOMContentLoaded', function() {
213
- const dataSelect = document.getElementById('existing_data');
214
- const previewSection = document.getElementById('data_preview_section');
215
- const previewContent = document.getElementById('data_preview_content');
216
-
217
- // Data preview functionality
218
- dataSelect.addEventListener('change', function() {
219
- const filename = dataSelect.value;
220
- if (!filename) {
221
- previewSection.style.display = 'none';
222
- previewContent.innerHTML = '<small class="text-muted">Select a data source to preview</small>';
223
- return;
224
- }
225
- fetch('{{ url_for("execute.data_preview", filename="FILENAME") }}'.replace('FILENAME', encodeURIComponent(filename)))
226
- .then(response => {
227
- if (!response.ok) throw new Error('Network response was not ok');
228
- return response.json();
229
- })
230
- .then(data => {
231
- previewSection.style.display = '';
232
- if (!data.rows || data.rows.length === 0) {
233
- previewContent.innerHTML = '<small class="text-muted">No data found in file.</small>';
234
- return;
235
- }
236
- let html = '<table class="table table-sm table-bordered mb-0"><thead><tr>';
237
- data.columns.forEach(col => html += `<th>${col}</th>`);
238
- html += '</tr></thead><tbody>';
239
- data.rows.forEach(row => {
240
- html += '<tr>';
241
- data.columns.forEach(col => html += `<td>${row[col] || ''}</td>`);
242
- html += '</tr>';
243
- });
244
- html += '</tbody></table>';
245
- previewContent.innerHTML = html;
246
- })
247
- .catch(() => {
248
- previewSection.style.display = '';
249
- previewContent.innerHTML = '<small class="text-danger">Failed to load preview.</small>';
210
+
211
+ {% if not no_deck_warning%}
212
+ <div class="input-group mb-3 mt-3">
213
+ <button class="form-control" type="submit" name="bo">Run</button>
214
+ </div>
215
+ {% endif %}
216
+ </div>
217
+ </form>
218
+
219
+ <script>
220
+ const optimizerSchema = {{ optimizer_schema|tojson }};
221
+
222
+ document.addEventListener('DOMContentLoaded', function() {
223
+ const dataSelect = document.getElementById('existing_data');
224
+ const previewSection = document.getElementById('data_preview_section');
225
+ const previewContent = document.getElementById('data_preview_content');
226
+
227
+ // Data preview functionality
228
+ dataSelect.addEventListener('change', function() {
229
+ const filename = dataSelect.value;
230
+ if (!filename) {
231
+ previewSection.style.display = 'none';
232
+ previewContent.innerHTML = '<small class="text-muted">Select a data source to preview</small>';
233
+ return;
234
+ }
235
+ fetch('{{ url_for("execute.data_preview", filename="FILENAME") }}'.replace('FILENAME', encodeURIComponent(filename)))
236
+ .then(response => {
237
+ if (!response.ok) throw new Error('Network response was not ok');
238
+ return response.json();
239
+ })
240
+ .then(data => {
241
+ previewSection.style.display = '';
242
+ if (!data.rows || data.rows.length === 0) {
243
+ previewContent.innerHTML = '<small class="text-muted">No data found in file.</small>';
244
+ return;
245
+ }
246
+ let html = '<table class="table table-sm table-bordered mb-0"><thead><tr>';
247
+ data.columns.forEach(col => html += `<th>${col}</th>`);
248
+ html += '</tr></thead><tbody>';
249
+ data.rows.forEach(row => {
250
+ html += '<tr>';
251
+ data.columns.forEach(col => html += `<td>${row[col] || ''}</td>`);
252
+ html += '</tr>';
250
253
  });
251
- });
254
+ html += '</tbody></table>';
255
+ previewContent.innerHTML = html;
256
+ })
257
+ .catch(() => {
258
+ previewSection.style.display = '';
259
+ previewContent.innerHTML = '<small class="text-danger">Failed to load preview.</small>';
260
+ });
252
261
  });
262
+ });
253
263
 
254
- function updateOptimizerInfo() {
255
- const optimizerSelect = document.getElementById('optimizer_type');
256
- const selectedOption = optimizerSelect.selectedOptions[0];
257
264
 
258
- if (!selectedOption || !selectedOption.value) {
259
- // Hide optimizer config and reset parameter types
260
- document.getElementById('optimizer_config_container').style.display = 'none';
261
- document.getElementById('optimizer_config_placeholder').style.display = 'block';
262
- resetParameterTypes();
263
- return;
264
- }
265
+ function updateOptimizerConfig(config) {
266
+ const container = document.getElementById('optimizer_config_container');
267
+ const placeholder = document.getElementById('optimizer_config_placeholder');
265
268
 
266
- // Update parameter types
267
- const parameterTypes = selectedOption.dataset.parameterTypes.split(',');
268
- updateParameterTypeOptions(parameterTypes);
269
+ console.log('Updating optimizer config:', config);
269
270
 
270
- // Update optimizer config - use the stored schema data instead
271
- const optimizerName = selectedOption.value;
272
- if (optimizerSchema[optimizerName] && optimizerSchema[optimizerName].optimizer_config) {
273
- updateOptimizerConfig(optimizerSchema[optimizerName].optimizer_config);
274
- } else {
275
- document.getElementById('optimizer_config_container').style.display = 'none';
276
- document.getElementById('optimizer_config_placeholder').style.display = 'block';
277
- }
271
+ if (!config || !config.step_1) {
272
+ console.error('Invalid optimizer config:', config);
273
+ container.style.display = 'none';
274
+ placeholder.style.display = 'block';
275
+ return;
278
276
  }
279
277
 
280
- function updateParameterTypeOptions(availableTypes) {
281
- const parameterTypeSelects = document.querySelectorAll('.parameter-type');
278
+ placeholder.style.display = 'none';
279
+ container.style.display = 'block';
280
+
281
+ // Update Step 1
282
+ const step1ModelSelect = document.getElementById('step1_model');
283
+ if (step1ModelSelect && config.step_1.model) {
284
+ step1ModelSelect.innerHTML = '';
285
+ config.step_1.model.forEach(model => {
286
+ const option = document.createElement('option');
287
+ option.value = model;
288
+ option.textContent = model;
289
+ step1ModelSelect.appendChild(option);
290
+ });
291
+ }
282
292
 
283
- parameterTypeSelects.forEach(select => {
284
- // Clear existing options
285
- select.innerHTML = '';
293
+ // Update Step 1 num_samples if exists
294
+ const step1NumSamplesRow = document.getElementById('step1_num_samples_row');
295
+ const step1NumSamplesInput = document.getElementById('step1_num_samples');
296
+ if (step1NumSamplesRow && step1NumSamplesInput) {
297
+ if (config.step_1.num_samples !== undefined) {
298
+ step1NumSamplesRow.style.display = '';
299
+ step1NumSamplesInput.value = config.step_1.num_samples;
300
+ } else {
301
+ step1NumSamplesRow.style.display = 'none';
302
+ }
303
+ }
286
304
 
287
- // Add available options based on optimizer
288
- availableTypes.forEach(type => {
305
+ // Update Step 2
306
+ if (config.step_2) {
307
+ const step2ModelSelect = document.getElementById('step2_model');
308
+ if (step2ModelSelect && config.step_2.model) {
309
+ step2ModelSelect.innerHTML = '';
310
+ config.step_2.model.forEach(model => {
289
311
  const option = document.createElement('option');
290
- option.value = type;
291
- option.textContent = type;
292
- if (type === 'range') {
293
- option.selected = true;
294
- }
295
- select.appendChild(option);
312
+ option.value = model;
313
+ option.textContent = model;
314
+ step2ModelSelect.appendChild(option);
296
315
  });
297
-
298
- // Update inputs for the current selection
299
- updateParameterInputs(select);
300
- });
316
+ }
301
317
  }
318
+ }
302
319
 
303
- function resetParameterTypes() {
304
- const parameterTypeSelects = document.querySelectorAll('.parameter-type');
320
+ document.addEventListener("DOMContentLoaded", () => {
321
+ console.log("Tab3: Init");
305
322
 
306
- parameterTypeSelects.forEach(select => {
307
- select.innerHTML = '';
308
- const defaultTypes = ['range', 'choice', 'fixed'];
309
- defaultTypes.forEach(type => {
310
- const option = document.createElement('option');
311
- option.value = type;
312
- option.textContent = type;
313
- if (type === 'range') {
314
- option.selected = true;
315
- }
316
- select.appendChild(option);
317
- });
323
+ const FORM_STATE_KEY = "bo_form_state";
324
+ const form = document.querySelector('form[name="bo"]');
325
+ const optimizerSelect = document.getElementById("optimizer_type");
326
+ let saveTimeout = null;
318
327
 
319
- updateParameterInputs(select);
320
- });
321
- }
328
+ const saveFormData = () => {
329
+ if (!form) return;
322
330
 
323
- function updateParameterInputs(selectElement) {
324
- const parameterRow = selectElement.closest('.parameter-row');
325
- const singleInput = parameterRow.querySelector('.single-input');
326
- const rangeInputs = parameterRow.querySelector('.range-inputs');
331
+ clearTimeout(saveTimeout);
332
+ saveTimeout = setTimeout(() => {
333
+ const formData = new FormData(form);
334
+ const data = {};
327
335
 
328
- if (selectElement.value === 'range') {
329
- singleInput.style.display = 'none';
330
- rangeInputs.style.display = 'block';
331
- } else {
332
- singleInput.style.display = 'block';
333
- rangeInputs.style.display = 'none';
336
+ // Convert FormData to object
337
+ for (let [key, value] of formData.entries()) {
338
+ data[key] = value;
339
+ }
340
+
341
+ try {
342
+ sessionStorage.setItem(FORM_STATE_KEY, JSON.stringify(data));
343
+ console.log('Tab3: Saved form data', Object.keys(data).length, 'fields');
344
+ } catch (e) {
345
+ console.warn('Tab3: Could not save form data:', e);
346
+ }
347
+ }, 500); // Debounce save
348
+ };
349
+
350
+ const loadSavedData = () => {
351
+ try {
352
+ const saved = sessionStorage.getItem(FORM_STATE_KEY);
353
+ if (saved) {
354
+ const data = JSON.parse(saved);
355
+ console.log('Tab3: Found saved data', Object.keys(data).length, 'fields');
356
+ return data;
357
+ }
358
+ } catch (e) {
359
+ console.warn('Tab3: Could not load saved data:', e);
360
+ }
361
+ return null;
362
+ };
363
+
364
+ const restoreState = () => {
365
+ const data = loadSavedData();
366
+ if (!data) {
367
+ console.log('Tab3: No saved data to restore');
368
+ return;
334
369
  }
335
- }
336
370
 
337
- function updateOptimizerConfig(config) {
338
- const container = document.getElementById('optimizer_config_container');
339
- const placeholder = document.getElementById('optimizer_config_placeholder');
371
+ console.log('Tab3: Restoring form state');
340
372
 
341
- console.log('Updating optimizer config:', config); // Debug log
373
+ // First restore the optimizer selection
374
+ if (data.optimizer_type && optimizerSelect) {
375
+ optimizerSelect.value = data.optimizer_type;
376
+ console.log('Tab3: Restored optimizer:', data.optimizer_type);
377
+ }
378
+
379
+ // Trigger optimizer update to populate step dropdowns
380
+ if (optimizerSelect && optimizerSelect.value) {
381
+ updateOptimizerInfo();
382
+ }
342
383
 
343
- if (!config || !config.step_1) {
344
- container.style.display = 'none';
345
- placeholder.style.display = 'block';
384
+ // Small delay to ensure dropdowns are populated
385
+ setTimeout(() => {
386
+ // Then restore all other form values (including step selections)
387
+ Object.entries(data).forEach(([key, value]) => {
388
+ const el = form.querySelector(`[name="${key}"]`);
389
+ if (el) {
390
+ el.value = value;
391
+ console.log('Tab3: Restored', key, '=', value);
392
+ }
393
+ });
394
+ }, 100);
395
+ };
396
+
397
+ const updateOptimizerInfo = () => {
398
+ const opt = optimizerSelect.value;
399
+ if (!opt) {
400
+ // Hide optimizer config when no optimizer selected
401
+ document.getElementById('optimizer_config_container').style.display = 'none';
402
+ document.getElementById('optimizer_config_placeholder').style.display = 'block';
346
403
  return;
347
404
  }
348
405
 
349
- placeholder.style.display = 'none';
350
- container.style.display = 'block';
406
+ const schema = optimizerSchema[opt];
407
+ if (!schema) return;
351
408
 
352
- // Update Step 1
353
- const step1ModelSelect = document.getElementById('step1_model');
354
- if (step1ModelSelect && config.step_1.model) {
355
- step1ModelSelect.innerHTML = '';
356
- config.step_1.model.forEach(model => {
357
- const option = document.createElement('option');
358
- option.value = model;
359
- option.textContent = model;
360
- step1ModelSelect.appendChild(option);
361
- });
409
+ console.log('Tab3: Updating optimizer info for', opt);
410
+
411
+ // Update optimizer config (step 1 and step 2)
412
+ if (schema.optimizer_config) {
413
+ updateOptimizerConfig(schema.optimizer_config);
362
414
  }
363
415
 
364
- // Update Step 1 num_samples if exists
365
- const step1NumSamplesRow = document.getElementById('step1_num_samples_row');
366
- const step1NumSamplesInput = document.getElementById('step1_num_samples');
367
- if (step1NumSamplesRow && step1NumSamplesInput) {
368
- if (config.step_1.num_samples !== undefined) {
369
- step1NumSamplesRow.style.display = '';
370
- step1NumSamplesInput.value = config.step_1.num_samples;
416
+ const supportsContinuous = schema.supports_continuous !== false;
417
+ const available = schema.parameter_types || ["range", "choice", "fixed"];
418
+
419
+ document.querySelectorAll(".parameter-type").forEach(sel => {
420
+ const currentValue = sel.value; // Preserve current value if valid
421
+ sel.innerHTML = available.map(t => `<option value="${t}">${t}</option>`).join("");
422
+
423
+ // Restore previous value if it's still valid, otherwise use default
424
+ if (available.includes(currentValue)) {
425
+ sel.value = currentValue;
371
426
  } else {
372
- step1NumSamplesRow.style.display = 'none';
427
+ sel.value = available.includes("range") ? "range" : available[0];
373
428
  }
374
- }
375
429
 
376
- // Update Step 2
377
- if (config.step_2) {
378
- const step2ModelSelect = document.getElementById('step2_model');
379
- if (step2ModelSelect && config.step_2.model) {
380
- step2ModelSelect.innerHTML = '';
381
- config.step_2.model.forEach(model => {
382
- const option = document.createElement('option');
383
- option.value = model;
384
- option.textContent = model;
385
- step2ModelSelect.appendChild(option);
386
- });
430
+ updateParameterInputs(sel, supportsContinuous);
431
+ });
432
+
433
+ document.getElementById("constraints_section").style.display =
434
+ schema.supports_constraints === false ? "none" : "block";
435
+
436
+ saveFormData();
437
+ };
438
+
439
+ const updateParameterInputs = (selectElement, supportsContinuous = true) => {
440
+ const row = selectElement.closest(".parameter-row");
441
+ const single = row.querySelector(".single-input");
442
+ const range = row.querySelector(".range-inputs");
443
+ const isRange = selectElement.value === "range";
444
+
445
+ // Toggle visibility
446
+ single.style.display = isRange ? "none" : "block";
447
+ range.style.display = isRange ? "block" : "none";
448
+
449
+ // Handle "step" input rule
450
+ const stepInput = range?.querySelector('input[name$="_step"]');
451
+ if (stepInput) {
452
+ if (!supportsContinuous) {
453
+ stepInput.required = true;
454
+ stepInput.placeholder = "Step (required)";
455
+ } else {
456
+ stepInput.required = false;
457
+ stepInput.placeholder = "Step (optional)";
387
458
  }
388
459
  }
389
- }
460
+ };
390
461
 
391
- // Initialize parameter inputs on page load
392
- document.addEventListener('DOMContentLoaded', function() {
393
- const parameterTypeSelects = document.querySelectorAll('.parameter-type');
394
- parameterTypeSelects.forEach(select => {
395
- updateParameterInputs(select);
462
+ // Bind events
463
+ if (optimizerSelect) {
464
+ optimizerSelect.addEventListener("change", () => {
465
+ updateOptimizerInfo();
466
+ saveFormData();
396
467
  });
397
- });
398
- </script>
468
+ }
469
+
470
+ if (form) {
471
+ // Save on any input change
472
+ form.addEventListener("input", saveFormData);
473
+ form.addEventListener("change", saveFormData);
474
+ }
475
+
476
+ document.querySelectorAll(".parameter-type").forEach(sel =>
477
+ sel.addEventListener("change", e => {
478
+ const opt = optimizerSelect.value;
479
+ const schema = opt ? optimizerSchema[opt] : null;
480
+ const supportsContinuous = schema ? (schema.supports_continuous !== false) : true;
481
+ updateParameterInputs(e.target, supportsContinuous);
482
+ saveFormData();
483
+ })
484
+ );
485
+
486
+ // Handle page unload
487
+ window.addEventListener('beforeunload', saveFormData);
488
+
489
+ console.log("Tab3: Init Done");
490
+
491
+ // Restore state after all event listeners are set up
492
+ restoreState();
493
+
494
+ // ============================================
495
+ // EXPOSE FUNCTIONS GLOBALLY FOR COORDINATION
496
+ // ============================================
497
+ window.saveBO = saveFormData;
498
+ window.loadBO = restoreState;
499
+ });
500
+
501
+ function addConstraintRow() {
502
+ const container = document.getElementById('constraints_container');
503
+ const newRow = document.createElement('div');
504
+ newRow.classList.add('row', 'align-items-center', 'mb-2', 'constraint-row');
505
+ newRow.innerHTML = `
506
+ <div class="col-10">
507
+ <input type="text" class="form-control form-control-sm" name="constraint_expr" placeholder="e.g. a + b <= 80">
508
+ </div>
509
+ <div class="col-2">
510
+ <button type="button" class="btn btn-outline-danger btn-sm" onclick="removeConstraintRow(this)">−</button>
511
+ </div>
512
+ `;
513
+ container.appendChild(newRow);
514
+ }
515
+ function removeConstraintRow(button) {
516
+ button.closest('.constraint-row').remove();
517
+ }
518
+ </script>
519
+
520
+ </div>