ivoryos 1.0.9__py3-none-any.whl → 1.4.4__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.
Files changed (107) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +17 -207
  3. ivoryos/app.py +154 -0
  4. ivoryos/config.py +1 -0
  5. ivoryos/optimizer/ax_optimizer.py +191 -0
  6. ivoryos/optimizer/base_optimizer.py +84 -0
  7. ivoryos/optimizer/baybe_optimizer.py +193 -0
  8. ivoryos/optimizer/nimo_optimizer.py +173 -0
  9. ivoryos/optimizer/registry.py +11 -0
  10. ivoryos/routes/auth/auth.py +43 -14
  11. ivoryos/routes/auth/templates/change_password.html +32 -0
  12. ivoryos/routes/control/control.py +101 -366
  13. ivoryos/routes/control/control_file.py +33 -0
  14. ivoryos/routes/control/control_new_device.py +152 -0
  15. ivoryos/routes/control/templates/controllers.html +193 -0
  16. ivoryos/routes/control/templates/controllers_new.html +112 -0
  17. ivoryos/routes/control/utils.py +40 -0
  18. ivoryos/routes/data/data.py +197 -0
  19. ivoryos/routes/data/templates/components/step_card.html +78 -0
  20. ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
  21. ivoryos/routes/data/templates/workflow_view.html +360 -0
  22. ivoryos/routes/design/__init__.py +4 -0
  23. ivoryos/routes/design/design.py +348 -657
  24. ivoryos/routes/design/design_file.py +68 -0
  25. ivoryos/routes/design/design_step.py +171 -0
  26. ivoryos/routes/design/templates/components/action_form.html +53 -0
  27. ivoryos/routes/design/templates/components/actions_panel.html +25 -0
  28. ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
  29. ivoryos/routes/design/templates/components/canvas.html +5 -0
  30. ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
  31. ivoryos/routes/design/templates/components/canvas_header.html +75 -0
  32. ivoryos/routes/design/templates/components/canvas_main.html +39 -0
  33. ivoryos/routes/design/templates/components/deck_selector.html +10 -0
  34. ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
  35. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  36. ivoryos/routes/design/templates/components/instruments_panel.html +88 -0
  37. ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
  38. ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
  39. ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
  40. ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
  41. ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
  42. ivoryos/routes/design/templates/components/modals.html +6 -0
  43. ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
  44. ivoryos/routes/design/templates/components/sidebar.html +15 -0
  45. ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
  46. ivoryos/routes/design/templates/experiment_builder.html +44 -0
  47. ivoryos/routes/execute/__init__.py +0 -0
  48. ivoryos/routes/execute/execute.py +377 -0
  49. ivoryos/routes/execute/execute_file.py +78 -0
  50. ivoryos/routes/execute/templates/components/error_modal.html +20 -0
  51. ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
  52. ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
  53. ivoryos/routes/execute/templates/components/run_panel.html +9 -0
  54. ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
  55. ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
  56. ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
  57. ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
  58. ivoryos/routes/execute/templates/experiment_run.html +30 -0
  59. ivoryos/routes/library/__init__.py +0 -0
  60. ivoryos/routes/library/library.py +157 -0
  61. ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
  62. ivoryos/routes/main/main.py +31 -3
  63. ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
  64. ivoryos/server.py +180 -0
  65. ivoryos/socket_handlers.py +52 -0
  66. ivoryos/static/ivoryos_logo.png +0 -0
  67. ivoryos/static/js/action_handlers.js +384 -0
  68. ivoryos/static/js/db_delete.js +23 -0
  69. ivoryos/static/js/script_metadata.js +39 -0
  70. ivoryos/static/js/socket_handler.js +40 -5
  71. ivoryos/static/js/sortable_design.js +107 -56
  72. ivoryos/static/js/ui_state.js +114 -0
  73. ivoryos/templates/base.html +67 -8
  74. ivoryos/utils/bo_campaign.py +180 -3
  75. ivoryos/utils/client_proxy.py +267 -36
  76. ivoryos/utils/db_models.py +300 -65
  77. ivoryos/utils/decorators.py +34 -0
  78. ivoryos/utils/form.py +63 -29
  79. ivoryos/utils/global_config.py +34 -1
  80. ivoryos/utils/nest_script.py +314 -0
  81. ivoryos/utils/py_to_json.py +295 -0
  82. ivoryos/utils/script_runner.py +599 -165
  83. ivoryos/utils/serilize.py +201 -0
  84. ivoryos/utils/task_runner.py +71 -21
  85. ivoryos/utils/utils.py +50 -6
  86. ivoryos/version.py +1 -1
  87. ivoryos-1.4.4.dist-info/METADATA +263 -0
  88. ivoryos-1.4.4.dist-info/RECORD +119 -0
  89. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
  90. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
  91. tests/unit/test_type_conversion.py +42 -0
  92. tests/unit/test_util.py +3 -0
  93. ivoryos/routes/control/templates/control/controllers.html +0 -78
  94. ivoryos/routes/control/templates/control/controllers_home.html +0 -55
  95. ivoryos/routes/control/templates/control/controllers_new.html +0 -89
  96. ivoryos/routes/database/database.py +0 -306
  97. ivoryos/routes/database/templates/database/step_card.html +0 -7
  98. ivoryos/routes/database/templates/database/workflow_view.html +0 -130
  99. ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
  100. ivoryos/routes/design/templates/design/experiment_run.html +0 -558
  101. ivoryos-1.0.9.dist-info/METADATA +0 -218
  102. ivoryos-1.0.9.dist-info/RECORD +0 -61
  103. /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
  104. /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
  105. /ivoryos/routes/{database → data}/__init__.py +0 -0
  106. /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
  107. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,60 @@
1
+ <!-- Main template structure -->
2
+ <ul class="nav nav-tabs" id="myTabs" role="tablist">
3
+ <li class="nav-item" role="presentation">
4
+ <a class="nav-link {{ 'disabled' if config_list else '' }} {{ 'active' if not config_list else '' }}" id="tab1-tab" data-bs-toggle="tab" href="#tab1" role="tab" aria-controls="tab1" aria-selected="false">Repeat</a>
5
+ </li>
6
+ <li class="nav-item" role="presentation">
7
+ <a class="nav-link {{ 'disabled' if not config_list else '' }} {{ 'active' if config_list else '' }}" id="tab2-tab" data-bs-toggle="tab" href="#tab2" role="tab" aria-controls="tab2" aria-selected="false">Configuration</a>
8
+ </li>
9
+ <li class="nav-item" role="presentation">
10
+ <a class="nav-link {{ 'disabled' if not config_list or not return_list else '' }}" id="tab3-tab" data-bs-toggle="tab" href="#tab3" role="tab" aria-controls="tab3" aria-selected="false">Bayesian Optimization</a>
11
+ </li>
12
+ </ul>
13
+ <div class="tab-content" id="myTabsContent">
14
+ {% include 'components/tab_repeat.html' %}
15
+ {% include 'components/tab_configuration.html' %}
16
+ {% include 'components/tab_bayesian.html' %}
17
+ </div>
18
+
19
+ <!-- ============================================ -->
20
+ <!-- SWITCH TO THE LAST ACTIVE TAB ON PAGE LOAD -->
21
+ <!-- ============================================ -->
22
+ <script>
23
+ (function() {
24
+ 'use strict';
25
+
26
+ // Store active tab when changed
27
+ document.addEventListener('shown.bs.tab', function(e) {
28
+ const tabId = e.target.id.replace('-tab', '');
29
+ if (tabId === 'tab2' || tabId === 'tab3') {
30
+ localStorage.setItem('ivoryosLastTab', tabId);
31
+ console.log('Saved last active tab:', tabId);
32
+ }
33
+
34
+ // Optional: run specific load logic per tab
35
+ if (tabId === 'tab2' && typeof window.loadConfigData === 'function') {
36
+ window.loadConfigData();
37
+ } else if (tabId === 'tab3' && typeof window.saveBO === 'function') {
38
+ window.saveBO();
39
+ }
40
+ });
41
+
42
+ // Restore tab from last session
43
+ document.addEventListener('DOMContentLoaded', function() {
44
+ const lastTab = localStorage.getItem('ivoryosLastTab');
45
+ const tabElement = lastTab ? document.getElementById(lastTab + '-tab') : null;
46
+
47
+ if (tabElement && !tabElement.classList.contains('disabled')) {
48
+ new bootstrap.Tab(tabElement).show();
49
+ console.log('Restored last active tab:', lastTab);
50
+ } else {
51
+ // Default to first non-disabled tab
52
+ const firstTab = document.querySelector('#myTabs .nav-link:not(.disabled)');
53
+ if (firstTab) {
54
+ new bootstrap.Tab(firstTab).show();
55
+ console.log('Defaulted to first available tab.');
56
+ }
57
+ }
58
+ });
59
+ })();
60
+ </script>
@@ -0,0 +1,520 @@
1
+ {# Bayesian optimization tab component #}
2
+
3
+ <div class="tab-pane fade" id="tab3" role="tabpanel" aria-labelledby="tab3-tab">
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>
20
+
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>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
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)">
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
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>
123
+ </div>
124
+ {% endfor %}
125
+
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">
133
+ </div>
134
+ <div class="col-2">
135
+ <button type="button" class="btn btn-outline-secondary btn-sm" onclick="addConstraintRow()">+</button>
136
+ </div>
137
+ </div>
138
+ </div>
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>
149
+ </div>
150
+ </div>
151
+
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;">
157
+
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>
173
+ </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:
177
+ </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="">
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
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>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ <div id="optimizer_config_placeholder">
205
+ <small class="text-muted">Select an optimizer to configure advanced settings</small>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
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>';
253
+ });
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
+ });
261
+ });
262
+ });
263
+
264
+
265
+ function updateOptimizerConfig(config) {
266
+ const container = document.getElementById('optimizer_config_container');
267
+ const placeholder = document.getElementById('optimizer_config_placeholder');
268
+
269
+ console.log('Updating optimizer config:', config);
270
+
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;
276
+ }
277
+
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
+ }
292
+
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
+ }
304
+
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 => {
311
+ const option = document.createElement('option');
312
+ option.value = model;
313
+ option.textContent = model;
314
+ step2ModelSelect.appendChild(option);
315
+ });
316
+ }
317
+ }
318
+ }
319
+
320
+ document.addEventListener("DOMContentLoaded", () => {
321
+ console.log("Tab3: Init");
322
+
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;
327
+
328
+ const saveFormData = () => {
329
+ if (!form) return;
330
+
331
+ clearTimeout(saveTimeout);
332
+ saveTimeout = setTimeout(() => {
333
+ const formData = new FormData(form);
334
+ const data = {};
335
+
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;
369
+ }
370
+
371
+ console.log('Tab3: Restoring form state');
372
+
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
+ }
383
+
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';
403
+ return;
404
+ }
405
+
406
+ const schema = optimizerSchema[opt];
407
+ if (!schema) return;
408
+
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);
414
+ }
415
+
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;
426
+ } else {
427
+ sel.value = available.includes("range") ? "range" : available[0];
428
+ }
429
+
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)";
458
+ }
459
+ }
460
+ };
461
+
462
+ // Bind events
463
+ if (optimizerSelect) {
464
+ optimizerSelect.addEventListener("change", () => {
465
+ updateOptimizerInfo();
466
+ saveFormData();
467
+ });
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>