ivoryos 1.2.5__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.
- docs/source/conf.py +84 -0
- ivoryos/__init__.py +16 -246
- ivoryos/app.py +154 -0
- ivoryos/optimizer/ax_optimizer.py +55 -28
- ivoryos/optimizer/base_optimizer.py +20 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +3 -1
- ivoryos/routes/auth/auth.py +35 -8
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +58 -28
- ivoryos/routes/control/control_file.py +12 -15
- ivoryos/routes/control/control_new_device.py +21 -11
- ivoryos/routes/control/templates/controllers.html +27 -0
- ivoryos/routes/control/utils.py +2 -0
- ivoryos/routes/data/data.py +110 -44
- ivoryos/routes/data/templates/components/step_card.html +78 -13
- ivoryos/routes/data/templates/workflow_view.html +343 -113
- ivoryos/routes/design/design.py +59 -10
- ivoryos/routes/design/design_file.py +3 -3
- ivoryos/routes/design/design_step.py +43 -17
- ivoryos/routes/design/templates/components/action_form.html +2 -2
- ivoryos/routes/design/templates/components/canvas_main.html +6 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +3 -0
- ivoryos/routes/execute/execute.py +82 -22
- ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
- ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
- ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
- ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
- ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
- ivoryos/routes/execute/templates/experiment_run.html +0 -264
- ivoryos/routes/library/library.py +9 -11
- ivoryos/routes/main/main.py +30 -2
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +1 -1
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +259 -88
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +29 -11
- ivoryos/templates/base.html +61 -2
- ivoryos/utils/bo_campaign.py +18 -17
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +286 -60
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +52 -19
- ivoryos/utils/global_config.py +21 -0
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +80 -10
- ivoryos/utils/script_runner.py +573 -189
- ivoryos/utils/task_runner.py +69 -22
- ivoryos/utils/utils.py +48 -5
- ivoryos/version.py +1 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- ivoryos-1.4.4.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/conftest.py +133 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_route_auth.py +80 -0
- tests/integration/test_route_control.py +94 -0
- tests/integration/test_route_database.py +61 -0
- tests/integration/test_route_design.py +36 -0
- tests/integration/test_route_main.py +35 -0
- tests/integration/test_sockets.py +26 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -0
- ivoryos/routes/api/api.py +0 -56
- ivoryos-1.2.5.dist-info/RECORD +0 -100
- ivoryos-1.2.5.dist-info/top_level.txt +0 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/licenses/LICENSE +0 -0
ivoryos/routes/main/main.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from flask import Blueprint, render_template, current_app, request, url_for
|
|
4
|
+
from flask_login import login_required, current_user
|
|
5
|
+
from werkzeug.utils import secure_filename, redirect
|
|
6
|
+
from ivoryos.utils.db_models import db
|
|
3
7
|
from ivoryos.version import __version__ as ivoryos_version
|
|
4
8
|
|
|
5
9
|
main = Blueprint('main', __name__, template_folder='templates')
|
|
@@ -40,3 +44,27 @@ def help_info():
|
|
|
40
44
|
ivoryos(__name__)
|
|
41
45
|
"""
|
|
42
46
|
return render_template('help.html', sample_deck=sample_deck)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@main.route('/customize-logo', methods=['POST'])
|
|
50
|
+
@login_required
|
|
51
|
+
def customize_logo():
|
|
52
|
+
if request.method == 'POST':
|
|
53
|
+
file = request.files.get('logo')
|
|
54
|
+
mode = request.form.get('mode')
|
|
55
|
+
|
|
56
|
+
if file and file.filename != '':
|
|
57
|
+
filename = secure_filename(file.filename)
|
|
58
|
+
|
|
59
|
+
USER_LOGO_DIR = os.path.join(current_app.static_folder, "user_logos")
|
|
60
|
+
os.makedirs(USER_LOGO_DIR, exist_ok=True)
|
|
61
|
+
filepath = os.path.join(USER_LOGO_DIR, filename)
|
|
62
|
+
file.save(filepath)
|
|
63
|
+
|
|
64
|
+
# Save to database
|
|
65
|
+
current_user.settings = {"logo_filename": filename, "logo_mode": mode}
|
|
66
|
+
# current_user.logo_mode = mode
|
|
67
|
+
db.session.commit()
|
|
68
|
+
|
|
69
|
+
return redirect(url_for('main.index'))
|
|
70
|
+
|
ivoryos/server.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
from flask import Blueprint
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Engine, event
|
|
9
|
+
|
|
10
|
+
# from ivoryos import BUILDING_BLOCKS
|
|
11
|
+
from ivoryos.app import create_app
|
|
12
|
+
from ivoryos.config import Config, get_config
|
|
13
|
+
from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
|
|
14
|
+
from ivoryos.routes.auth.auth import login_manager
|
|
15
|
+
from ivoryos.routes.control.control import global_config
|
|
16
|
+
from ivoryos.socket_handlers import socketio
|
|
17
|
+
from ivoryos.utils import utils
|
|
18
|
+
from ivoryos.utils.db_models import db, User
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
|
|
22
|
+
|
|
23
|
+
@event.listens_for(Engine, "connect")
|
|
24
|
+
def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
|
|
25
|
+
if isinstance(dbapi_connection, sqlite3.Connection):
|
|
26
|
+
cursor = dbapi_connection.cursor()
|
|
27
|
+
cursor.execute("PRAGMA foreign_keys=ON")
|
|
28
|
+
cursor.close()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@login_manager.user_loader
|
|
33
|
+
def load_user(user_id):
|
|
34
|
+
"""
|
|
35
|
+
This function is called by Flask-Login on every request to get the
|
|
36
|
+
current user object from the user ID stored in the session.
|
|
37
|
+
"""
|
|
38
|
+
# The correct implementation is to fetch the user from the database.
|
|
39
|
+
return db.session.get(User, user_id)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, model=None,
|
|
46
|
+
config: Config = None,
|
|
47
|
+
logger: Union[str, list] = None,
|
|
48
|
+
logger_output_name: str = None,
|
|
49
|
+
enable_design: bool = True,
|
|
50
|
+
blueprint_plugins: Union[list, Blueprint] = [],
|
|
51
|
+
exclude_names: list = [],
|
|
52
|
+
notification_handler=None,
|
|
53
|
+
optimizer_registry: dict = None,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Start ivoryOS app server.
|
|
57
|
+
|
|
58
|
+
:param module: module name, __name__ for current module
|
|
59
|
+
:param host: host address, defaults to 0.0.0.0
|
|
60
|
+
:param port: port, defaults to None, and will use 8000
|
|
61
|
+
:param debug: debug mode, defaults to None (True)
|
|
62
|
+
:param llm_server: llm server, defaults to None.
|
|
63
|
+
:param model: llm model, defaults to None. If None, app will run without text-to-code feature
|
|
64
|
+
:param config: config class, defaults to None
|
|
65
|
+
:param logger: logger name of list of logger names, defaults to None
|
|
66
|
+
:param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
|
|
67
|
+
:param enable_design: enable design canvas, database and workflow execution
|
|
68
|
+
:param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
|
|
69
|
+
:param exclude_names: list[str] module names to exclude from parsing
|
|
70
|
+
:param notification_handler: notification handler function
|
|
71
|
+
"""
|
|
72
|
+
app = create_app(config_class=config or get_config()) # Create app instance using factory function
|
|
73
|
+
|
|
74
|
+
# plugins = load_installed_plugins(app, socketio)
|
|
75
|
+
plugins = []
|
|
76
|
+
if blueprint_plugins:
|
|
77
|
+
config_plugins = load_plugins(blueprint_plugins, app, socketio)
|
|
78
|
+
plugins.extend(config_plugins)
|
|
79
|
+
|
|
80
|
+
def inject_nav_config():
|
|
81
|
+
"""Make NAV_CONFIG available globally to all templates."""
|
|
82
|
+
return dict(
|
|
83
|
+
enable_design=enable_design,
|
|
84
|
+
plugins=plugins,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
app.context_processor(inject_nav_config)
|
|
88
|
+
port = port or int(os.environ.get("PORT", 8000))
|
|
89
|
+
debug = debug if debug is not None else app.config.get('DEBUG', True)
|
|
90
|
+
|
|
91
|
+
app.config["LOGGERS"] = logger
|
|
92
|
+
app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
|
|
93
|
+
logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
|
|
94
|
+
dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
|
|
95
|
+
if optimizer_registry:
|
|
96
|
+
global_config.optimizers = optimizer_registry
|
|
97
|
+
else:
|
|
98
|
+
global_config.optimizers = OPTIMIZER_REGISTRY
|
|
99
|
+
if module:
|
|
100
|
+
app.config["MODULE"] = module
|
|
101
|
+
app.config["OFF_LINE"] = False
|
|
102
|
+
global_config.deck = sys.modules[module]
|
|
103
|
+
global_config.building_blocks = utils.create_block_snapshot()
|
|
104
|
+
global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
|
|
105
|
+
output_path=dummy_deck_path,
|
|
106
|
+
save=True,
|
|
107
|
+
exclude_names=exclude_names
|
|
108
|
+
)
|
|
109
|
+
global_config.api_variables = utils.create_module_snapshot(global_config.deck)
|
|
110
|
+
|
|
111
|
+
else:
|
|
112
|
+
app.config["OFF_LINE"] = True
|
|
113
|
+
if model:
|
|
114
|
+
app.config["ENABLE_LLM"] = True
|
|
115
|
+
app.config["LLM_MODEL"] = model
|
|
116
|
+
app.config["LLM_SERVER"] = llm_server
|
|
117
|
+
utils.install_and_import('openai')
|
|
118
|
+
from ivoryos.utils.llm_agent import LlmAgent
|
|
119
|
+
global_config.agent = LlmAgent(host=llm_server, model=model,
|
|
120
|
+
output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
|
|
121
|
+
else:
|
|
122
|
+
app.config["ENABLE_LLM"] = False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --- Logger registration ---
|
|
126
|
+
if logger:
|
|
127
|
+
if isinstance(logger, str):
|
|
128
|
+
logger = [logger] # convert single logger to list
|
|
129
|
+
elif not isinstance(logger, list):
|
|
130
|
+
raise TypeError("logger must be a string or a list of strings.")
|
|
131
|
+
|
|
132
|
+
for log_name in logger:
|
|
133
|
+
utils.start_logger(socketio, log_filename=logger_path, logger_name=log_name)
|
|
134
|
+
|
|
135
|
+
# --- Notification handler registration ---
|
|
136
|
+
if notification_handler:
|
|
137
|
+
|
|
138
|
+
# make it a list if a single function is passed
|
|
139
|
+
if callable(notification_handler):
|
|
140
|
+
notification_handler = [notification_handler]
|
|
141
|
+
|
|
142
|
+
if not isinstance(notification_handler, list):
|
|
143
|
+
raise ValueError("notification_handlers must be a callable or a list of callables.")
|
|
144
|
+
|
|
145
|
+
# validate all items are callable
|
|
146
|
+
for handler in notification_handler:
|
|
147
|
+
if not callable(handler):
|
|
148
|
+
raise TypeError(f"Handler {handler} is not callable.")
|
|
149
|
+
global_config.register_notification(handler)
|
|
150
|
+
|
|
151
|
+
# TODO in case Python 3.12 or higher doesn't log URL
|
|
152
|
+
# if sys.version_info >= (3, 12):
|
|
153
|
+
# ip = utils.get_local_ip()
|
|
154
|
+
# print(f"Server running at http://localhost:{port}")
|
|
155
|
+
# if not ip == "127.0.0.1":
|
|
156
|
+
# print(f"Server running at http://{ip}:{port}")
|
|
157
|
+
socketio.run(app, host=host, port=port, debug=debug, use_reloader=False, allow_unsafe_werkzeug=True)
|
|
158
|
+
# return app
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
|
|
163
|
+
"""
|
|
164
|
+
Dynamically load installed plugins and attach Flask-SocketIO.
|
|
165
|
+
:param blueprints: Union[list, Blueprint] list of Blueprint objects or a single Blueprint object
|
|
166
|
+
:param app: Flask application instance
|
|
167
|
+
:param socketio: Flask-SocketIO instance
|
|
168
|
+
:return: list of plugin names
|
|
169
|
+
"""
|
|
170
|
+
plugin_names = []
|
|
171
|
+
if not isinstance(blueprints, list):
|
|
172
|
+
blueprints = [blueprints]
|
|
173
|
+
for blueprint in blueprints:
|
|
174
|
+
# If the plugin has an `init_socketio()` function, pass socketio
|
|
175
|
+
if hasattr(blueprint, 'init_socketio'):
|
|
176
|
+
blueprint.init_socketio(socketio)
|
|
177
|
+
plugin_names.append(blueprint.name)
|
|
178
|
+
app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
|
|
179
|
+
return plugin_names
|
|
180
|
+
|
ivoryos/socket_handlers.py
CHANGED
|
Binary file
|
|
@@ -1,6 +1,148 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// STATE MANAGEMENT
|
|
3
|
+
// ============================================================================
|
|
1
4
|
|
|
5
|
+
let previousHtmlState = null; // Store previous instrument panel state
|
|
6
|
+
let lastFocusedElement = null; // Track focus for modal management
|
|
2
7
|
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// MODE & BATCH MANAGEMENT
|
|
10
|
+
// ============================================================================
|
|
3
11
|
|
|
12
|
+
function getMode() {
|
|
13
|
+
return sessionStorage.getItem("mode") || "single";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function setMode(mode, triggerUpdate = true) {
|
|
17
|
+
sessionStorage.setItem("mode", mode);
|
|
18
|
+
|
|
19
|
+
const modeButtons = document.querySelectorAll(".mode-toggle");
|
|
20
|
+
const batchOptions = document.getElementById("batch-options");
|
|
21
|
+
|
|
22
|
+
modeButtons.forEach(b => b.classList.toggle("active", b.dataset.mode === mode));
|
|
23
|
+
|
|
24
|
+
if (batchOptions) {
|
|
25
|
+
batchOptions.style.display = (mode === "batch") ? "inline-flex" : "none";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (triggerUpdate) updateCode();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getBatch() {
|
|
32
|
+
return sessionStorage.getItem("batch") || "sample";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setBatch(batch, triggerUpdate = true) {
|
|
36
|
+
sessionStorage.setItem("batch", batch);
|
|
37
|
+
|
|
38
|
+
const batchButtons = document.querySelectorAll(".batch-toggle");
|
|
39
|
+
batchButtons.forEach(b => b.classList.toggle("active", b.dataset.batch === batch));
|
|
40
|
+
|
|
41
|
+
if (triggerUpdate) updateCode();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// CODE OVERLAY MANAGEMENT
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
async function updateCode() {
|
|
49
|
+
try {
|
|
50
|
+
const params = new URLSearchParams({ mode: getMode(), batch: getBatch() });
|
|
51
|
+
const res = await fetch(scriptCompileUrl + "?" + params.toString());
|
|
52
|
+
if (!res.ok) return;
|
|
53
|
+
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
const codeElem = document.getElementById("python-code");
|
|
56
|
+
|
|
57
|
+
codeElem.removeAttribute("data-highlighted"); // Reset highlight.js flag
|
|
58
|
+
codeElem.textContent = data.code['script'] || "# No code found";
|
|
59
|
+
|
|
60
|
+
if (window.hljs) {
|
|
61
|
+
hljs.highlightElement(codeElem);
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error("Error updating code:", err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function initializeCodeOverlay() {
|
|
69
|
+
const codeElem = document.getElementById("python-code");
|
|
70
|
+
const copyBtn = document.getElementById("copy-code");
|
|
71
|
+
const downloadBtn = document.getElementById("download-code");
|
|
72
|
+
|
|
73
|
+
if (!copyBtn || !downloadBtn) return; // Elements don't exist
|
|
74
|
+
|
|
75
|
+
// Remove old listeners by cloning (prevents duplicate bindings)
|
|
76
|
+
const newCopyBtn = copyBtn.cloneNode(true);
|
|
77
|
+
const newDownloadBtn = downloadBtn.cloneNode(true);
|
|
78
|
+
copyBtn.parentNode.replaceChild(newCopyBtn, copyBtn);
|
|
79
|
+
downloadBtn.parentNode.replaceChild(newDownloadBtn, downloadBtn);
|
|
80
|
+
|
|
81
|
+
// Copy to clipboard
|
|
82
|
+
newCopyBtn.addEventListener("click", () => {
|
|
83
|
+
navigator.clipboard.writeText(codeElem.textContent)
|
|
84
|
+
.then(() => alert("Code copied!"))
|
|
85
|
+
.catch(err => console.error("Failed to copy", err));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Download current code
|
|
89
|
+
newDownloadBtn.addEventListener("click", () => {
|
|
90
|
+
const blob = new Blob([codeElem.textContent], { type: "text/plain" });
|
|
91
|
+
const url = URL.createObjectURL(blob);
|
|
92
|
+
const a = document.createElement("a");
|
|
93
|
+
a.href = url;
|
|
94
|
+
a.download = "script.py";
|
|
95
|
+
a.click();
|
|
96
|
+
URL.revokeObjectURL(url);
|
|
97
|
+
});
|
|
98
|
+
updateCode();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// UI UPDATE FUNCTIONS
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
function updateActionCanvas(html) {
|
|
106
|
+
document.getElementById("canvas-action-wrapper").innerHTML = html;
|
|
107
|
+
initializeCanvas();
|
|
108
|
+
|
|
109
|
+
const mode = getMode();
|
|
110
|
+
const batch = getBatch();
|
|
111
|
+
|
|
112
|
+
// Rebind event handlers for mode/batch toggles
|
|
113
|
+
document.querySelectorAll(".mode-toggle").forEach(btn => {
|
|
114
|
+
btn.addEventListener("click", () => setMode(btn.dataset.mode));
|
|
115
|
+
});
|
|
116
|
+
document.querySelectorAll(".batch-toggle").forEach(btn => {
|
|
117
|
+
btn.addEventListener("click", () => setBatch(btn.dataset.batch));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Restore toggle UI state (without triggering updates)
|
|
121
|
+
setMode(mode, false);
|
|
122
|
+
setBatch(batch, false);
|
|
123
|
+
|
|
124
|
+
// Reinitialize code overlay buttons
|
|
125
|
+
initializeCodeOverlay();
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function updateInstrumentPanel(link) {
|
|
130
|
+
const url = link.dataset.getUrl;
|
|
131
|
+
|
|
132
|
+
fetch(url)
|
|
133
|
+
.then(res => res.json())
|
|
134
|
+
.then(data => {
|
|
135
|
+
if (data.html) {
|
|
136
|
+
document.getElementById("sidebar-wrapper").innerHTML = data.html;
|
|
137
|
+
initializeDragHandlers();
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.catch(err => console.error("Error updating instrument panel:", err));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// WORKFLOW MANAGEMENT
|
|
145
|
+
// ============================================================================
|
|
4
146
|
|
|
5
147
|
function saveWorkflow(link) {
|
|
6
148
|
const url = link.dataset.postUrl;
|
|
@@ -14,9 +156,7 @@ function saveWorkflow(link) {
|
|
|
14
156
|
.then(res => res.json())
|
|
15
157
|
.then(data => {
|
|
16
158
|
if (data.success) {
|
|
17
|
-
|
|
18
|
-
flash("Workflow saved successfully", "success");
|
|
19
|
-
window.location.reload(); // or update the UI dynamically
|
|
159
|
+
window.location.reload();
|
|
20
160
|
} else {
|
|
21
161
|
alert("Failed to save workflow: " + data.error);
|
|
22
162
|
}
|
|
@@ -27,21 +167,30 @@ function saveWorkflow(link) {
|
|
|
27
167
|
});
|
|
28
168
|
}
|
|
29
169
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
170
|
+
function clearDraft() {
|
|
171
|
+
fetch(scriptDeleteUrl, {
|
|
172
|
+
method: "DELETE",
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
},
|
|
176
|
+
})
|
|
34
177
|
.then(res => res.json())
|
|
35
178
|
.then(data => {
|
|
36
|
-
if (data.
|
|
37
|
-
|
|
38
|
-
|
|
179
|
+
if (data.success) {
|
|
180
|
+
window.location.reload();
|
|
181
|
+
} else {
|
|
182
|
+
alert("Failed to clear draft");
|
|
39
183
|
}
|
|
40
184
|
})
|
|
185
|
+
.catch(error => console.error("Failed to clear draft", error));
|
|
41
186
|
}
|
|
42
187
|
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// ACTION MANAGEMENT (CRUD Operations)
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
43
192
|
function addMethodToDesign(event, form) {
|
|
44
|
-
event.preventDefault();
|
|
193
|
+
event.preventDefault();
|
|
45
194
|
|
|
46
195
|
const formData = new FormData(form);
|
|
47
196
|
|
|
@@ -61,31 +210,58 @@ function addMethodToDesign(event, form) {
|
|
|
61
210
|
.catch(error => console.error('Error:', error));
|
|
62
211
|
}
|
|
63
212
|
|
|
213
|
+
function editAction(uuid) {
|
|
214
|
+
if (!uuid) {
|
|
215
|
+
console.error('Invalid UUID');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
64
218
|
|
|
65
|
-
|
|
66
|
-
document.getElementById(
|
|
67
|
-
initializeCanvas(); // Reinitialize canvas functionality
|
|
68
|
-
document.querySelectorAll('#pythonCodeOverlay pre code').forEach((block) => {
|
|
69
|
-
hljs.highlightElement(block);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let lastFocusedElement = null;
|
|
219
|
+
// Store current state for rollback
|
|
220
|
+
previousHtmlState = document.getElementById('instrument-panel').innerHTML;
|
|
75
221
|
|
|
222
|
+
fetch(scriptStepUrl.replace('0', uuid), {
|
|
223
|
+
method: 'GET',
|
|
224
|
+
headers: {
|
|
225
|
+
'Content-Type': 'application/json'
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
.then(response => {
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
return response.json().then(err => {
|
|
231
|
+
if (err.warning) {
|
|
232
|
+
alert(err.warning);
|
|
233
|
+
}
|
|
234
|
+
// Restore panel so user isn't stuck
|
|
235
|
+
if (previousHtmlState) {
|
|
236
|
+
document.getElementById('instrument-panel').innerHTML = previousHtmlState;
|
|
237
|
+
previousHtmlState = null;
|
|
238
|
+
}
|
|
239
|
+
throw new Error("Step fetch failed: " + response.status);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return response.text();
|
|
243
|
+
})
|
|
244
|
+
.then(html => {
|
|
245
|
+
document.getElementById('instrument-panel').innerHTML = html;
|
|
76
246
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
247
|
+
// Set up back button
|
|
248
|
+
const backButton = document.getElementById('back');
|
|
249
|
+
if (backButton) {
|
|
250
|
+
backButton.addEventListener('click', function(e) {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
if (previousHtmlState) {
|
|
253
|
+
document.getElementById('instrument-panel').innerHTML = previousHtmlState;
|
|
254
|
+
previousHtmlState = null;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
.catch(error => console.error('Error:', error));
|
|
85
260
|
}
|
|
86
261
|
|
|
87
262
|
function submitEditForm(event) {
|
|
88
263
|
event.preventDefault();
|
|
264
|
+
|
|
89
265
|
const form = event.target;
|
|
90
266
|
const formData = new FormData(form);
|
|
91
267
|
|
|
@@ -96,43 +272,21 @@ function submitEditForm(event) {
|
|
|
96
272
|
.then(response => response.text())
|
|
97
273
|
.then(html => {
|
|
98
274
|
if (html) {
|
|
99
|
-
// Update only the action list
|
|
100
275
|
updateActionCanvas(html);
|
|
101
276
|
|
|
277
|
+
// Restore previous instrument panel state
|
|
102
278
|
if (previousHtmlState) {
|
|
103
279
|
document.getElementById('instrument-panel').innerHTML = previousHtmlState;
|
|
104
|
-
previousHtmlState = null;
|
|
280
|
+
previousHtmlState = null;
|
|
105
281
|
}
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
.catch(error => {
|
|
109
|
-
console.error('Error:', error);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
282
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
method: "DELETE",
|
|
116
|
-
headers: {
|
|
117
|
-
"Content-Type": "application/json",
|
|
118
|
-
},
|
|
119
|
-
})
|
|
120
|
-
.then(res => res.json())
|
|
121
|
-
.then(data => {
|
|
122
|
-
if (data.success) {
|
|
123
|
-
window.location.reload();
|
|
124
|
-
} else {
|
|
125
|
-
alert("Failed to clear draft");
|
|
283
|
+
// Check for warnings
|
|
284
|
+
showWarningIfExists(html);
|
|
126
285
|
}
|
|
127
286
|
})
|
|
128
|
-
.catch(error => console.error(
|
|
287
|
+
.catch(error => console.error('Error:', error));
|
|
129
288
|
}
|
|
130
289
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let previousHtmlState = null; // Store the previous state
|
|
135
|
-
|
|
136
290
|
function duplicateAction(uuid) {
|
|
137
291
|
if (!uuid) {
|
|
138
292
|
console.error('Invalid UUID');
|
|
@@ -145,69 +299,86 @@ function duplicateAction(uuid) {
|
|
|
145
299
|
'Content-Type': 'application/json'
|
|
146
300
|
}
|
|
147
301
|
})
|
|
148
|
-
|
|
149
302
|
.then(response => response.text())
|
|
150
303
|
.then(html => {
|
|
151
304
|
updateActionCanvas(html);
|
|
305
|
+
showWarningIfExists(html);
|
|
152
306
|
})
|
|
153
307
|
.catch(error => console.error('Error:', error));
|
|
154
308
|
}
|
|
155
309
|
|
|
156
|
-
function
|
|
310
|
+
function deleteAction(uuid) {
|
|
157
311
|
if (!uuid) {
|
|
158
312
|
console.error('Invalid UUID');
|
|
159
313
|
return;
|
|
160
314
|
}
|
|
161
315
|
|
|
162
|
-
// Save current state before fetching new content
|
|
163
|
-
previousHtmlState = document.getElementById('instrument-panel').innerHTML;
|
|
164
|
-
|
|
165
316
|
fetch(scriptStepUrl.replace('0', uuid), {
|
|
166
|
-
method: '
|
|
317
|
+
method: 'DELETE',
|
|
167
318
|
headers: {
|
|
168
319
|
'Content-Type': 'application/json'
|
|
169
320
|
}
|
|
170
321
|
})
|
|
171
322
|
.then(response => response.text())
|
|
172
323
|
.then(html => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Add click handler for back button
|
|
176
|
-
document.getElementById('back').addEventListener('click', function(e) {
|
|
177
|
-
e.preventDefault();
|
|
178
|
-
if (previousHtmlState) {
|
|
179
|
-
document.getElementById('instrument-panel').innerHTML = previousHtmlState;
|
|
180
|
-
previousHtmlState = null; // Clear the stored state
|
|
181
|
-
}
|
|
182
|
-
});
|
|
324
|
+
updateActionCanvas(html);
|
|
325
|
+
showWarningIfExists(html);
|
|
183
326
|
})
|
|
184
327
|
.catch(error => console.error('Error:', error));
|
|
185
328
|
}
|
|
186
329
|
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// MODAL MANAGEMENT
|
|
332
|
+
// ============================================================================
|
|
187
333
|
|
|
334
|
+
function hideModal() {
|
|
335
|
+
if (document.activeElement) {
|
|
336
|
+
document.activeElement.blur();
|
|
337
|
+
}
|
|
188
338
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
339
|
+
$('#dropModal').modal('hide');
|
|
340
|
+
|
|
341
|
+
if (lastFocusedElement) {
|
|
342
|
+
lastFocusedElement.focus();
|
|
193
343
|
}
|
|
344
|
+
}
|
|
194
345
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
.
|
|
202
|
-
.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// UTILITY FUNCTIONS
|
|
348
|
+
// ============================================================================
|
|
349
|
+
|
|
350
|
+
function showWarningIfExists(html) {
|
|
351
|
+
const parser = new DOMParser();
|
|
352
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
353
|
+
const warningDiv = doc.querySelector('#warning');
|
|
354
|
+
|
|
355
|
+
if (warningDiv && warningDiv.textContent.trim()) {
|
|
356
|
+
alert(warningDiv.textContent.trim());
|
|
357
|
+
}
|
|
207
358
|
}
|
|
208
359
|
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// INITIALIZATION
|
|
362
|
+
// ============================================================================
|
|
209
363
|
|
|
364
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
365
|
+
const mode = getMode();
|
|
366
|
+
const batch = getBatch();
|
|
210
367
|
|
|
368
|
+
// Set up mode/batch toggle listeners
|
|
369
|
+
document.querySelectorAll(".mode-toggle").forEach(btn => {
|
|
370
|
+
btn.addEventListener("click", () => setMode(btn.dataset.mode));
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
document.querySelectorAll(".batch-toggle").forEach(btn => {
|
|
374
|
+
btn.addEventListener("click", () => setBatch(btn.dataset.batch));
|
|
375
|
+
});
|
|
211
376
|
|
|
377
|
+
// Restore UI state (without triggering updates)
|
|
378
|
+
setMode(mode, false);
|
|
379
|
+
setBatch(batch, false);
|
|
212
380
|
|
|
381
|
+
// Initialize code overlay
|
|
382
|
+
initializeCodeOverlay();
|
|
213
383
|
|
|
384
|
+
});
|