ivoryzhang commited on
Commit
68e6de8
·
1 Parent(s): 007efe1

add websocket warning

Browse files
Files changed (49) hide show
  1. demo_code_plugin/plugin.py +0 -3
  2. demo_code_plugin/templates/example.html +8 -0
  3. ivoryos/__init__.py +0 -210
  4. ivoryos/config.py +0 -66
  5. ivoryos/routes/__init__.py +0 -0
  6. ivoryos/routes/auth/__init__.py +0 -0
  7. ivoryos/routes/auth/auth.py +0 -97
  8. ivoryos/routes/auth/templates/auth/login.html +0 -25
  9. ivoryos/routes/auth/templates/auth/signup.html +0 -32
  10. ivoryos/routes/control/__init__.py +0 -0
  11. ivoryos/routes/control/control.py +0 -429
  12. ivoryos/routes/control/templates/control/controllers.html +0 -78
  13. ivoryos/routes/control/templates/control/controllers_home.html +0 -55
  14. ivoryos/routes/control/templates/control/controllers_new.html +0 -89
  15. ivoryos/routes/database/__init__.py +0 -0
  16. ivoryos/routes/database/database.py +0 -306
  17. ivoryos/routes/database/templates/database/scripts_database.html +0 -83
  18. ivoryos/routes/database/templates/database/step_card.html +0 -7
  19. ivoryos/routes/database/templates/database/workflow_database.html +0 -103
  20. ivoryos/routes/database/templates/database/workflow_view.html +0 -130
  21. ivoryos/routes/design/__init__.py +0 -0
  22. ivoryos/routes/design/design.py +0 -792
  23. ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
  24. ivoryos/routes/design/templates/design/experiment_run.html +0 -558
  25. ivoryos/routes/main/__init__.py +0 -0
  26. ivoryos/routes/main/main.py +0 -42
  27. ivoryos/routes/main/templates/main/help.html +0 -141
  28. ivoryos/routes/main/templates/main/home.html +0 -103
  29. ivoryos/static/favicon.ico +0 -0
  30. ivoryos/static/js/overlay.js +0 -12
  31. ivoryos/static/js/socket_handler.js +0 -125
  32. ivoryos/static/js/sortable_card.js +0 -24
  33. ivoryos/static/js/sortable_design.js +0 -105
  34. ivoryos/static/logo.webp +0 -0
  35. ivoryos/static/style.css +0 -211
  36. ivoryos/templates/base.html +0 -157
  37. ivoryos/utils/__init__.py +0 -0
  38. ivoryos/utils/bo_campaign.py +0 -87
  39. ivoryos/utils/client_proxy.py +0 -57
  40. ivoryos/utils/db_models.py +0 -700
  41. ivoryos/utils/form.py +0 -560
  42. ivoryos/utils/global_config.py +0 -87
  43. ivoryos/utils/input_types.md +0 -14
  44. ivoryos/utils/llm_agent.py +0 -183
  45. ivoryos/utils/script_runner.py +0 -336
  46. ivoryos/utils/task_runner.py +0 -81
  47. ivoryos/utils/utils.py +0 -422
  48. ivoryos/version.py +0 -1
  49. requirements.txt +1 -11
demo_code_plugin/plugin.py CHANGED
@@ -1,6 +1,3 @@
1
- import eventlet
2
- eventlet.monkey_patch()
3
-
4
  from flask import render_template, Blueprint, current_app
5
  import os
6
 
 
 
 
 
1
  from flask import render_template, Blueprint, current_app
2
  import os
3
 
demo_code_plugin/templates/example.html CHANGED
@@ -79,6 +79,14 @@
79
  For authentication, user passwords are securely hashed using <code>bcrypt</code> before being stored.
80
  Use this demo to explore IvoryOS features!
81
  </p>
 
 
 
 
 
 
 
 
82
  <h2>Source Code</h2>
83
  <pre><code class="language-python">{{ code|e }}</code></pre>
84
  </div>
 
79
  For authentication, user passwords are securely hashed using <code>bcrypt</code> before being stored.
80
  Use this demo to explore IvoryOS features!
81
  </p>
82
+ <div id="progress-warning" style="border: 1px solid #f5c2c7; background-color: #f8d7da; color: #842029; padding: 1rem; border-radius: 0.5rem; margin: 1rem 0; font-size: 0.95rem;">
83
+ ⚠️ <strong>Note:</strong> Real-time progress tracking via <code>SocketIO</code> is currently <strong>not working</strong> on Hugging Face Spaces.
84
+ <br>
85
+ This is due to limited WebSocket support in the Hugging Face hosting environment.
86
+ <br>
87
+ Workflow will still work, results can be tracked via the <strong>Data</strong> tab.
88
+ <br>
89
+ </div>
90
  <h2>Source Code</h2>
91
  <pre><code class="language-python">{{ code|e }}</code></pre>
92
  </div>
ivoryos/__init__.py DELETED
@@ -1,210 +0,0 @@
1
- import os
2
- import sys
3
- from typing import Union
4
- import eventlet
5
- eventlet.monkey_patch()
6
-
7
- from flask import Flask, redirect, url_for, g, Blueprint
8
-
9
- from ivoryos.config import Config, get_config
10
- from ivoryos.routes.auth.auth import auth, login_manager
11
- from ivoryos.routes.control.control import control
12
- from ivoryos.routes.database.database import database
13
- from ivoryos.routes.design.design import design, socketio
14
- from ivoryos.routes.main.main import main
15
- # from ivoryos.routes.monitor.monitor import monitor
16
- from ivoryos.utils import utils
17
- from ivoryos.utils.db_models import db, User
18
- from ivoryos.utils.global_config import GlobalConfig
19
- from ivoryos.utils.script_runner import ScriptRunner
20
- from ivoryos.version import __version__ as ivoryos_version
21
- from importlib.metadata import entry_points
22
-
23
- global_config = GlobalConfig()
24
- from sqlalchemy import event
25
- from sqlalchemy.engine import Engine
26
- import sqlite3
27
-
28
-
29
- @event.listens_for(Engine, "connect")
30
- def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
31
- if isinstance(dbapi_connection, sqlite3.Connection):
32
- cursor = dbapi_connection.cursor()
33
- cursor.execute("PRAGMA foreign_keys=ON")
34
- cursor.close()
35
-
36
-
37
- url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
38
- app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
39
- app.register_blueprint(main, url_prefix=url_prefix)
40
- app.register_blueprint(auth, url_prefix=url_prefix)
41
- app.register_blueprint(control, url_prefix=url_prefix)
42
- app.register_blueprint(design, url_prefix=url_prefix)
43
- app.register_blueprint(database, url_prefix=url_prefix)
44
-
45
- @login_manager.user_loader
46
- def load_user(user_id):
47
- """
48
- This function is called by Flask-Login on every request to get the
49
- current user object from the user ID stored in the session.
50
- """
51
- # The correct implementation is to fetch the user from the database.
52
- return db.session.get(User, user_id)
53
-
54
-
55
- def create_app(config_class=None):
56
- """
57
- create app, init database
58
- """
59
- app.config.from_object(config_class or 'config.get_config()')
60
- os.makedirs(app.config["OUTPUT_FOLDER"], exist_ok=True)
61
- # Initialize extensions
62
- socketio.init_app(app, cookie=None, logger=True, engineio_logger=True)
63
- login_manager.init_app(app)
64
- login_manager.login_view = "auth.login"
65
- db.init_app(app)
66
-
67
- # Create database tables
68
- with app.app_context():
69
- db.create_all()
70
-
71
- # Additional setup
72
- utils.create_gui_dir(app.config['OUTPUT_FOLDER'])
73
-
74
- # logger_list = app.config["LOGGERS"]
75
- logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
76
- logger = utils.start_logger(socketio, 'gui_logger', logger_path)
77
-
78
- @app.before_request
79
- def before_request():
80
- """
81
- Called before
82
-
83
- """
84
- g.logger = logger
85
- g.socketio = socketio
86
-
87
- @app.route('/')
88
- def redirect_to_prefix():
89
- return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
90
-
91
- return app
92
-
93
-
94
- def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, model=None,
95
- config: Config = None,
96
- logger: Union[str, list] = None,
97
- logger_output_name: str = None,
98
- enable_design: bool = True,
99
- blueprint_plugins: Union[list, Blueprint] = [],
100
- exclude_names: list = [],
101
- ):
102
- """
103
- Start ivoryOS app server.
104
-
105
- :param module: module name, __name__ for current module
106
- :param host: host address, defaults to 0.0.0.0
107
- :param port: port, defaults to None, and will use 8000
108
- :param debug: debug mode, defaults to None (True)
109
- :param llm_server: llm server, defaults to None.
110
- :param model: llm model, defaults to None. If None, app will run without text-to-code feature
111
- :param config: config class, defaults to None
112
- :param logger: logger name of list of logger names, defaults to None
113
- :param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
114
- :param enable_design: enable design canvas, database and workflow execution
115
- :param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
116
- :param exclude_names: list[str] module names to exclude from parsing
117
- """
118
- app = create_app(config_class=config or get_config()) # Create app instance using factory function
119
-
120
- # plugins = load_installed_plugins(app, socketio)
121
- plugins = []
122
- if blueprint_plugins:
123
- config_plugins = load_plugins(blueprint_plugins, app, socketio)
124
- plugins.extend(config_plugins)
125
-
126
- def inject_nav_config():
127
- """Make NAV_CONFIG available globally to all templates."""
128
- return dict(
129
- enable_design=enable_design,
130
- plugins=plugins,
131
- )
132
-
133
- app.context_processor(inject_nav_config)
134
- port = port or int(os.environ.get("PORT", 8000))
135
- debug = debug if debug is not None else app.config.get('DEBUG', True)
136
-
137
- app.config["LOGGERS"] = logger
138
- app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
139
- logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
140
- dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
141
-
142
- if module:
143
- app.config["MODULE"] = module
144
- app.config["OFF_LINE"] = False
145
- global_config.deck = sys.modules[module]
146
- global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
147
- output_path=dummy_deck_path,
148
- save=True,
149
- exclude_names=exclude_names
150
- )
151
- else:
152
- app.config["OFF_LINE"] = True
153
- if model:
154
- app.config["ENABLE_LLM"] = True
155
- app.config["LLM_MODEL"] = model
156
- app.config["LLM_SERVER"] = llm_server
157
- utils.install_and_import('openai')
158
- from ivoryos.utils.llm_agent import LlmAgent
159
- global_config.agent = LlmAgent(host=llm_server, model=model,
160
- output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
161
- else:
162
- app.config["ENABLE_LLM"] = False
163
- if logger and type(logger) is str:
164
- utils.start_logger(socketio, log_filename=logger_path, logger_name=logger)
165
- elif type(logger) is list:
166
- for log in logger:
167
- utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
168
-
169
- # in case Python 3.12 or higher doesn't log URL
170
- if sys.version_info >= (3, 12):
171
- ip = utils.get_ip_address()
172
- print(f"Server running at http://localhost:{port}")
173
- if not ip == "127.0.0.1":
174
- print(f"Server running at http://{ip}:{port}")
175
- socketio.run(app, host=host, port=port, debug=debug, use_reloader=False, allow_unsafe_werkzeug=True)
176
- # return app
177
-
178
-
179
- def load_installed_plugins(app, socketio):
180
- """
181
- Dynamically load installed plugins and attach Flask-SocketIO.
182
- """
183
- plugin_names = []
184
- for entry_point in entry_points().get("ivoryos.plugins", []):
185
- plugin = entry_point.load()
186
-
187
- # If the plugin has an `init_socketio()` function, pass socketio
188
- if hasattr(plugin, 'init_socketio'):
189
- plugin.init_socketio(socketio)
190
-
191
- plugin_names.append(entry_point.name)
192
- app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
193
-
194
- return plugin_names
195
-
196
-
197
- def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
198
- """
199
- Dynamically load installed plugins and attach Flask-SocketIO.
200
- """
201
- plugin_names = []
202
- if not isinstance(blueprints, list):
203
- blueprints = [blueprints]
204
- for blueprint in blueprints:
205
- # If the plugin has an `init_socketio()` function, pass socketio
206
- if hasattr(blueprint, 'init_socketio'):
207
- blueprint.init_socketio(socketio)
208
- plugin_names.append(blueprint.name)
209
- app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
210
- return plugin_names
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/config.py DELETED
@@ -1,66 +0,0 @@
1
- import os
2
- from dotenv import load_dotenv
3
-
4
- # Load environment variables from .env file
5
- load_dotenv()
6
-
7
-
8
- class Config:
9
- SECRET_KEY = os.getenv('SECRET_KEY', 'default_secret_key')
10
- OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', None)
11
-
12
- OUTPUT_FOLDER = os.path.join(os.path.abspath(os.curdir), 'ivoryos_data')
13
- CSV_FOLDER = os.path.join(OUTPUT_FOLDER, 'config_csv/')
14
- SCRIPT_FOLDER = os.path.join(OUTPUT_FOLDER, 'scripts/')
15
- DATA_FOLDER = os.path.join(OUTPUT_FOLDER, 'results/')
16
- DUMMY_DECK = os.path.join(OUTPUT_FOLDER, 'pseudo_deck/')
17
- LLM_OUTPUT = os.path.join(OUTPUT_FOLDER, 'llm_output/')
18
- DECK_HISTORY = os.path.join(OUTPUT_FOLDER, 'deck_history.txt')
19
- LOGGERS_PATH = "default.log"
20
-
21
- SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(OUTPUT_FOLDER, 'ivoryos.db')}"
22
- SQLALCHEMY_TRACK_MODIFICATIONS = False
23
-
24
- ENABLE_LLM = True if OPENAI_API_KEY else False
25
- OFF_LINE = True
26
-
27
-
28
- class DevelopmentConfig(Config):
29
- DEBUG = True
30
-
31
-
32
- class ProductionConfig(Config):
33
- DEBUG = False
34
-
35
-
36
- class TestingConfig(Config):
37
- DEBUG = True
38
- TESTING = True
39
- SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' # Use an in-memory SQLite database for tests
40
- WTF_CSRF_ENABLED = False # Disable CSRF for testing forms
41
-
42
-
43
-
44
- class DemoConfig(Config):
45
- DEBUG = False
46
- SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
47
- OUTPUT_FOLDER = os.path.join(os.path.abspath(os.curdir), '/tmp/ivoryos_data')
48
- CSV_FOLDER = os.path.join(OUTPUT_FOLDER, 'config_csv/')
49
- SCRIPT_FOLDER = os.path.join(OUTPUT_FOLDER, 'scripts/')
50
- DATA_FOLDER = os.path.join(OUTPUT_FOLDER, 'results/')
51
- DUMMY_DECK = os.path.join(OUTPUT_FOLDER, 'pseudo_deck/')
52
- LLM_OUTPUT = os.path.join(OUTPUT_FOLDER, 'llm_output/')
53
- DECK_HISTORY = os.path.join(OUTPUT_FOLDER, 'deck_history.txt')
54
- # session and cookies
55
- SESSION_COOKIE_SECURE = True
56
- SESSION_COOKIE_SAMESITE = "None"
57
- SESSION_COOKIE_HTTPONLY = True
58
-
59
- def get_config(env='dev'):
60
- if env == 'production':
61
- return ProductionConfig()
62
- elif env == 'testing':
63
- return TestingConfig()
64
- elif env == 'demo':
65
- return DemoConfig()
66
- return DevelopmentConfig()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/__init__.py DELETED
File without changes
ivoryos/routes/auth/__init__.py DELETED
File without changes
ivoryos/routes/auth/auth.py DELETED
@@ -1,97 +0,0 @@
1
- from flask import Blueprint, redirect, url_for, flash, request, render_template, session
2
- from flask_login import login_required, login_user, logout_user, LoginManager
3
- import bcrypt
4
-
5
- from ivoryos.utils.db_models import Script, User, db
6
- from ivoryos.utils.utils import post_script_file
7
- login_manager = LoginManager()
8
-
9
- auth = Blueprint('auth', __name__, template_folder='templates/auth')
10
-
11
-
12
- @auth.route('/auth/login', methods=['GET', 'POST'])
13
- def login():
14
- """
15
- .. :quickref: User; login user
16
-
17
- .. http:get:: /login
18
-
19
- load user login form.
20
-
21
- .. http:post:: /login
22
-
23
- :form username: username
24
- :form password: password
25
- :status 302: and then redirects to homepage
26
- :status 401: incorrect password, redirects to :http:get:`/ivoryos/login`
27
- """
28
- if request.method == 'POST':
29
- username = request.form.get('username')
30
- password = request.form.get('password')
31
-
32
- # session.query(User, User.name).all()
33
- user = db.session.query(User).filter(User.username == username).first()
34
- input_password = password.encode('utf-8')
35
- # if user and bcrypt.checkpw(input_password, user.hashPassword.encode('utf-8')):
36
- if user and bcrypt.checkpw(input_password, user.hashPassword):
37
- # password.encode("utf-8")
38
- # user = User(username, password.encode("utf-8"))
39
- login_user(user)
40
- session['user'] = username
41
- script_file = Script(author=username)
42
- session["script"] = script_file.as_dict()
43
- session['hidden_functions'], session['card_order'], session['prompt'] = {}, {}, {}
44
- session['autofill'] = False
45
- post_script_file(script_file)
46
- return redirect(url_for('main.index'))
47
- else:
48
- flash("Incorrect username or password")
49
- return redirect(url_for('auth.login')), 401
50
- return render_template('login.html')
51
-
52
-
53
- @auth.route('/auth/signup', methods=['GET', 'POST'])
54
- def signup():
55
- """
56
- .. :quickref: User; signup for a new account
57
-
58
- .. http:get:: /signup
59
-
60
- load user sighup
61
-
62
- .. http:post:: /signup
63
-
64
- :form username: username
65
- :form password: password
66
- :status 302: and then redirects to :http:get:`/ivoryos/login`
67
- :status 409: when user already exists, redirects to :http:get:`/ivoryos/signup`
68
- """
69
- if request.method == 'POST':
70
- username = request.form.get('username')
71
- password = request.form.get('password')
72
-
73
- # Query the database to see if the user already exists.
74
- existing_user = User.query.filter_by(username=username).first()
75
-
76
- if existing_user:
77
- flash("User already exists :(", "error")
78
- return render_template('signup.html'), 409
79
- hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
80
- user = User(username, hashed)
81
- db.session.add(user)
82
- db.session.commit()
83
- return redirect(url_for('auth.login'))
84
- return render_template('signup.html')
85
-
86
-
87
- @auth.route("/auth/logout")
88
- @login_required
89
- def logout():
90
- """
91
- .. :quickref: User; logout the user
92
-
93
- logout the current user, clear session info, and redirect to the login page.
94
- """
95
- logout_user()
96
- session.clear()
97
- return redirect(url_for('auth.login'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/auth/templates/auth/login.html DELETED
@@ -1,25 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Login{% endblock %}
3
-
4
-
5
- {% block body %}
6
- <div class= "login">
7
- <div class="bg-white rounded shadow-sm flex-fill">
8
- <div class="p-4" style="align-items: center">
9
- <h5>Log in</h5>
10
- <form role="form" method='POST' name="login" action="{{ url_for('auth.login') }}">
11
- <div class="input-group mb-3">
12
- <label class="input-group-text" for="username">Username</label>
13
- <input class="form-control" type="text" id="username" name="username">
14
- </div>
15
- <div class="input-group mb-3">
16
- <label class="input-group-text" for="password">Password</label>
17
- <input class="form-control" type="password" id="password" name="password">
18
- </div>
19
- <button type="submit" class="btn btn-secondary" name="login" style="width: 100%;">login</button>
20
- </form>
21
- <p class="message">Not registered? <a href="{{ url_for('auth.signup') }}">Create a new account</a>
22
- </div>
23
- </div>
24
- </div>
25
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/auth/templates/auth/signup.html DELETED
@@ -1,32 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Signup{% endblock %}
3
-
4
-
5
- {% block body %}
6
- <div class= "login">
7
- <div class="bg-white rounded shadow-sm flex-fill">
8
- <div class="p-4" style="align: center">
9
- <h5>Create a new account</h5>
10
- <form role="form" method='POST' name="signup" action="{{ url_for('auth.signup') }}">
11
-
12
- <div class="input-group mb-3">
13
- <label class="input-group-text" for="username">Username</label>
14
- <input class="form-control" type="text" id="username" name="username" required>
15
- </div>
16
- <div class="input-group mb-3">
17
- <label class="input-group-text" for="password">Password</label>
18
- <input class="form-control" type="password" id="password" name="password" required>
19
- </div>
20
- {# <div class="input-group mb-3">#}
21
- {# <label class="input-group-text" for="confirm_password">Confirm Password</label>#}
22
- {# <input class="form-control" type="confirm_password" id="confirm_password" name="confirm_password">#}
23
- {# </div>#}
24
-
25
- <button type="submit" class="btn btn-secondary" name="login" style="width: 100%;">Sign up</button>
26
- </form>
27
- <p class="message" >Already registered? <a href="{{ url_for('auth.login') }}">Sign In</a></p>
28
- </div>
29
- </div>
30
- </div>
31
-
32
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/control/__init__.py DELETED
File without changes
ivoryos/routes/control/control.py DELETED
@@ -1,429 +0,0 @@
1
- import os
2
-
3
- from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify, \
4
- send_file
5
- from flask_login import login_required
6
-
7
- from ivoryos.utils.client_proxy import export_to_python, create_function
8
- from ivoryos.utils.global_config import GlobalConfig
9
- from ivoryos.utils import utils
10
- from ivoryos.utils.form import create_form_from_module, format_name
11
- from ivoryos.utils.task_runner import TaskRunner
12
-
13
- global_config = GlobalConfig()
14
- runner = TaskRunner()
15
-
16
- control = Blueprint('control', __name__, template_folder='templates/control')
17
-
18
-
19
- @control.route("/control/home/deck", strict_slashes=False)
20
- @login_required
21
- def deck_controllers():
22
- """
23
- .. :quickref: Direct Control; controls home interface
24
-
25
- deck control home interface for listing all deck instruments
26
-
27
- .. http:get:: /control/home/deck
28
- """
29
- deck_variables = global_config.deck_snapshot.keys()
30
- deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
31
- return render_template('controllers_home.html', defined_variables=deck_variables, deck=True, history=deck_list)
32
-
33
-
34
- @control.route("/control/new/", strict_slashes=False)
35
- @control.route("/control/new/<instrument>", methods=['GET', 'POST'])
36
- @login_required
37
- def new_controller(instrument=None):
38
- """
39
- .. :quickref: Direct Control; connect to a new device
40
-
41
- interface for connecting a new <instrument>
42
-
43
- .. http:get:: /control/new/
44
-
45
- :param instrument: instrument name
46
- :type instrument: str
47
-
48
- .. http:post:: /control/new/
49
-
50
- :form device_name: module instance name (e.g. my_instance = MyClass())
51
- :form kwargs: dynamic module initialization kwargs fields
52
-
53
- """
54
- device = None
55
- args = None
56
- if instrument:
57
-
58
- device = find_instrument_by_name(instrument)
59
- args = utils.inspect.signature(device.__init__)
60
-
61
- if request.method == 'POST':
62
- device_name = request.form.get("device_name", "")
63
- if device_name and device_name in globals():
64
- flash("Device name is defined. Try another name, or leave it as blank to auto-configure")
65
- return render_template('controllers_new.html', instrument=instrument,
66
- api_variables=global_config.api_variables,
67
- device=device, args=args, defined_variables=global_config.defined_variables)
68
- if device_name == "":
69
- device_name = device.__name__.lower() + "_"
70
- num = 1
71
- while device_name + str(num) in global_config.defined_variables:
72
- num += 1
73
- device_name = device_name + str(num)
74
- kwargs = request.form.to_dict()
75
- kwargs.pop("device_name")
76
- for i in kwargs:
77
- if kwargs[i] in global_config.defined_variables:
78
- kwargs[i] = global_config.defined_variables[kwargs[i]]
79
- try:
80
- utils.convert_config_type(kwargs, device.__init__.__annotations__, is_class=True)
81
- except Exception as e:
82
- flash(e)
83
- try:
84
- global_config.defined_variables[device_name] = device(**kwargs)
85
- # global_config.defined_variables.add(device_name)
86
- return redirect(url_for('control.controllers_home'))
87
- except Exception as e:
88
- flash(e)
89
- return render_template('controllers_new.html', instrument=instrument, api_variables=global_config.api_variables,
90
- device=device, args=args, defined_variables=global_config.defined_variables)
91
-
92
-
93
- @control.route("/control/home/temp", strict_slashes=False)
94
- @login_required
95
- def controllers_home():
96
- """
97
- .. :quickref: Direct Control; temp control home interface
98
-
99
- temporarily connected devices home interface for listing all instruments
100
-
101
- .. http:get:: /control/home/temp
102
-
103
- """
104
- # defined_variables = parse_deck(deck)
105
- defined_variables = global_config.defined_variables.keys()
106
- return render_template('controllers_home.html', defined_variables=defined_variables)
107
-
108
-
109
- @control.route("/control/<instrument>/methods", methods=['GET', 'POST'])
110
- @login_required
111
- def controllers(instrument: str):
112
- """
113
- .. :quickref: Direct Control; control interface
114
-
115
- control interface for selected <instrument>
116
-
117
- .. http:get:: /control/<instrument>/methods
118
-
119
- :param instrument: instrument name
120
- :type instrument: str
121
-
122
- .. http:post:: /control/<instrument>/methods
123
-
124
- :form hidden_name: function name (hidden field)
125
- :form kwargs: dynamic kwargs field
126
-
127
- """
128
- inst_object = find_instrument_by_name(instrument)
129
- _forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
130
- functions = list(_forms.keys())
131
-
132
- order = get_session_by_instrument('card_order', instrument)
133
- hidden_functions = get_session_by_instrument('hide_function', instrument)
134
-
135
- for function in functions:
136
- if function not in hidden_functions and function not in order:
137
- order.append(function)
138
- post_session_by_instrument('card_order', instrument, order)
139
- forms = {name: _forms[name] for name in order if name in _forms}
140
- if request.method == 'POST':
141
- all_kwargs = request.form.copy()
142
- method_name = all_kwargs.pop("hidden_name", None)
143
- # if method_name is not None:
144
- form = forms.get(method_name)
145
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
146
- function_executable = getattr(inst_object, method_name)
147
- if form and form.validate_on_submit():
148
- try:
149
- kwargs.pop("hidden_name")
150
- output = runner.run_single_step(instrument, method_name, kwargs, wait=True,
151
- current_app=current_app._get_current_object())
152
- # output = function_executable(**kwargs)
153
- flash(f"\nRun Success! Output value: {output}.")
154
- except Exception as e:
155
- flash(e.__str__())
156
- else:
157
- flash(form.errors)
158
- return render_template('controllers.html', instrument=instrument, forms=forms, format_name=format_name)
159
-
160
- @control.route("/control/download", strict_slashes=False)
161
- @login_required
162
- def download_proxy():
163
- """
164
- .. :quickref: Direct Control; download proxy interface
165
-
166
- download proxy interface
167
-
168
- .. http:get:: /control/download
169
- """
170
- snapshot = global_config.deck_snapshot.copy()
171
- class_definitions = {}
172
- # Iterate through each instrument in the snapshot
173
- for instrument_key, instrument_data in snapshot.items():
174
- # Iterate through each function associated with the current instrument
175
- for function_key, function_data in instrument_data.items():
176
- # Convert the function signature to a string representation
177
- function_data['signature'] = str(function_data['signature'])
178
- class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
179
- class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
180
- # Export the generated class definitions to a .py script
181
- export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
182
- filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
183
- return send_file(os.path.abspath(filepath), as_attachment=True)
184
-
185
- @control.route("/api/control/", strict_slashes=False, methods=['GET'])
186
- @control.route("/api/control/<instrument>", methods=['POST'])
187
- def backend_control(instrument: str=None):
188
- """
189
- .. :quickref: Backend Control; backend control
190
-
191
- backend control through http requests
192
-
193
- .. http:get:: /api/control/
194
-
195
- :param instrument: instrument name
196
- :type instrument: str
197
-
198
- .. http:post:: /api/control/
199
-
200
- """
201
- if instrument:
202
- inst_object = find_instrument_by_name(instrument)
203
- forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
204
-
205
- if request.method == 'POST':
206
- method_name = request.form.get("hidden_name", None)
207
- form = forms.get(method_name, None)
208
- if form:
209
- kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
210
- wait = request.form.get("hidden_wait", "true") == "true"
211
- output = runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
212
- current_app=current_app._get_current_object())
213
- return jsonify(output), 200
214
-
215
- snapshot = global_config.deck_snapshot.copy()
216
- # Iterate through each instrument in the snapshot
217
- for instrument_key, instrument_data in snapshot.items():
218
- # Iterate through each function associated with the current instrument
219
- for function_key, function_data in instrument_data.items():
220
- # Convert the function signature to a string representation
221
- function_data['signature'] = str(function_data['signature'])
222
- return jsonify(snapshot), 200
223
-
224
- # @control.route("/api/control", strict_slashes=False, methods=['GET'])
225
- # def backend_client():
226
- # """
227
- # .. :quickref: Backend Control; get snapshot
228
- #
229
- # backend control through http requests
230
- #
231
- # .. http:get:: /api/control/summary
232
- # """
233
- # # Create a snapshot of the current deck configuration
234
- # snapshot = global_config.deck_snapshot.copy()
235
- #
236
- # # Iterate through each instrument in the snapshot
237
- # for instrument_key, instrument_data in snapshot.items():
238
- # # Iterate through each function associated with the current instrument
239
- # for function_key, function_data in instrument_data.items():
240
- # # Convert the function signature to a string representation
241
- # function_data['signature'] = str(function_data['signature'])
242
- # return jsonify(snapshot), 200
243
-
244
-
245
- @control.route("/control/import/module", methods=['POST'])
246
- def import_api():
247
- """
248
- .. :quickref: Advanced Features; Manually import API module(s)
249
-
250
- importing other Python modules
251
-
252
- .. http:post:: /control/import/module
253
-
254
- :form filepath: API (Python class) module filepath
255
-
256
- import the module and redirect to :http:get:`/ivoryos/control/new/`
257
-
258
- """
259
- filepath = request.form.get('filepath')
260
- # filepath.replace('\\', '/')
261
- name = os.path.split(filepath)[-1].split('.')[0]
262
- try:
263
- spec = utils.importlib.util.spec_from_file_location(name, filepath)
264
- module = utils.importlib.util.module_from_spec(spec)
265
- spec.loader.exec_module(module)
266
- classes = utils.inspect.getmembers(module, utils.inspect.isclass)
267
- if len(classes) == 0:
268
- flash("Invalid import: no class found in the path")
269
- return redirect(url_for("control.controllers_home"))
270
- for i in classes:
271
- globals()[i[0]] = i[1]
272
- global_config.api_variables.add(i[0])
273
- # should handle path error and file type error
274
- except Exception as e:
275
- flash(e.__str__())
276
- return redirect(url_for("control.new_controller"))
277
-
278
-
279
- # @control.route("/disconnect", methods=["GET"])
280
- # @control.route("/disconnect/<device_name>", methods=["GET"])
281
- # def disconnect(device_name=None):
282
- # """TODO handle disconnect device"""
283
- # if device_name:
284
- # try:
285
- # exec(device_name + ".disconnect()")
286
- # except Exception:
287
- # pass
288
- # global_config.defined_variables.remove(device_name)
289
- # globals().pop(device_name)
290
- # return redirect(url_for('control.controllers_home'))
291
- #
292
- # deck_variables = ["deck." + var for var in set(dir(deck))
293
- # if not (var.startswith("_") or var[0].isupper() or var.startswith("repackage"))
294
- # and not type(eval("deck." + var)).__module__ == 'builtins']
295
- # for i in deck_variables:
296
- # try:
297
- # exec(i + ".disconnect()")
298
- # except Exception:
299
- # pass
300
- # globals()["deck"] = None
301
- # return redirect(url_for('control.deck_controllers'))
302
-
303
-
304
- @control.route("/control/import/deck", methods=['POST'])
305
- def import_deck():
306
- """
307
- .. :quickref: Advanced Features; Manually import a deck
308
-
309
- .. http:post:: /control/import_deck
310
-
311
- :form filepath: deck module filepath
312
-
313
- import the module and redirect to the previous page
314
-
315
- """
316
- script = utils.get_script_file()
317
- filepath = request.form.get('filepath')
318
- session['dismiss'] = request.form.get('dismiss')
319
- update = request.form.get('update')
320
- back = request.referrer
321
- if session['dismiss']:
322
- return redirect(back)
323
- name = os.path.split(filepath)[-1].split('.')[0]
324
- try:
325
- module = utils.import_module_by_filepath(filepath=filepath, name=name)
326
- utils.save_to_history(filepath, current_app.config["DECK_HISTORY"])
327
- module_sigs = utils.create_deck_snapshot(module, save=update, output_path=current_app.config["DUMMY_DECK"])
328
- if not len(module_sigs) > 0:
329
- flash("Invalid hardware deck, connect instruments in deck script", "error")
330
- return redirect(url_for("control.deck_controllers"))
331
- global_config.deck = module
332
- global_config.deck_snapshot = module_sigs
333
-
334
- if script.deck is None:
335
- script.deck = module.__name__
336
- # file path error exception
337
- except Exception as e:
338
- flash(e.__str__())
339
- return redirect(back)
340
-
341
-
342
- @control.route('/control/<instrument>/save-order', methods=['POST'])
343
- def save_order(instrument: str):
344
- """
345
- .. :quickref: Control Customization; Save functions' order
346
-
347
- .. http:post:: /control/save-order
348
-
349
- save function drag and drop order for the given <instrument>
350
-
351
- """
352
- # Save the new order for the specified group to session
353
- data = request.json
354
- post_session_by_instrument('card_order', instrument, data['order'])
355
- return '', 204
356
-
357
-
358
- @control.route('/control/<instrument>/<function>/hide')
359
- def hide_function(instrument, function):
360
- """
361
- .. :quickref: Control Customization; Hide function
362
-
363
- .. http:get:: //control/<instrument>/<function>/hide
364
-
365
- Hide the given <instrument> and <function>
366
-
367
- """
368
- back = request.referrer
369
- functions = get_session_by_instrument("hidden_functions", instrument)
370
- order = get_session_by_instrument("card_order", instrument)
371
- if function not in functions:
372
- functions.append(function)
373
- order.remove(function)
374
- post_session_by_instrument('hidden_functions', instrument, functions)
375
- post_session_by_instrument('card_order', instrument, order)
376
- return redirect(back)
377
-
378
-
379
- @control.route('/control/<instrument>/<function>/unhide')
380
- def remove_hidden(instrument: str, function: str):
381
- """
382
- .. :quickref: Control Customization; Remove a hidden function
383
-
384
- .. http:get:: /control/<instrument>/<function>/unhide
385
-
386
- Un-hide the given <instrument> and <function>
387
-
388
- """
389
- back = request.referrer
390
- functions = get_session_by_instrument("hidden_functions", instrument)
391
- order = get_session_by_instrument("card_order", instrument)
392
- if function in functions:
393
- functions.remove(function)
394
- order.append(function)
395
- post_session_by_instrument('hidden_functions', instrument, functions)
396
- post_session_by_instrument('card_order', instrument, order)
397
- return redirect(back)
398
-
399
-
400
- def get_session_by_instrument(session_name, instrument):
401
- """get data from session by instrument"""
402
- session_object = session.get(session_name, {})
403
- functions = session_object.get(instrument, [])
404
- return functions
405
-
406
-
407
- def post_session_by_instrument(session_name, instrument, data):
408
- """
409
- save new data to session by instrument
410
- :param session_name: "card_order" or "hidden_functions"
411
- :param instrument: function name of class object
412
- :param data: order list or hidden function list
413
- """
414
- session_object = session.get(session_name, {})
415
- session_object[instrument] = data
416
- session[session_name] = session_object
417
-
418
-
419
- def find_instrument_by_name(name: str):
420
- """
421
- find instrument class object by instance name
422
- """
423
- if name.startswith("deck"):
424
- name = name.replace("deck.", "")
425
- return getattr(global_config.deck, name)
426
- elif name in global_config.defined_variables:
427
- return global_config.defined_variables[name]
428
- elif name in globals():
429
- return globals()[name]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/control/templates/control/controllers.html DELETED
@@ -1,78 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Controller for {{instrument}}{% endblock %}
3
- {% block body %}
4
- <div id="overlay" class="overlay">
5
- <div>
6
- <h3 id="overlay-text"></h3>
7
- <div class="spinner-border" role="status"></div>
8
- </div>
9
- </div>
10
- <h1>{{instrument}} controller</h1>
11
- {% set hidden = session.get('hidden_functions', {}) %}
12
- <div class="grid-container" id="sortable-grid">
13
- {% for function, form in forms.items() %}
14
-
15
- {% set hidden_instrument = hidden.get(instrument, []) %}
16
- {% if function not in hidden_instrument %}
17
- <div class="card" id="{{function}}">
18
- <div class="bg-white rounded shadow-sm flex-fill">
19
- <i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top" title='{{ form.hidden_name.description or "Docstring is not available" }}' ></i>
20
- <a style="float: right" aria-label="Close" href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"><i class="bi bi-eye-slash-fill"></i></a>
21
- <div class="form-control" style="border: none">
22
- <form role="form" method='POST' name="{{function}}" id="{{function}}">
23
- <div class="form-group">
24
- {{ form.hidden_tag() }}
25
- {% for field in form %}
26
- {% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
27
- <div class="input-group mb-3">
28
- <label class="input-group-text">{{ field.label.text }}</label>
29
- {% if field.type == "SubmitField" %}
30
- {{ field(class="btn btn-dark") }}
31
- {% elif field.type == "BooleanField" %}
32
- {{ field(class="form-check-input") }}
33
- {% else %}
34
- {{ field(class="form-control") }}
35
- {% endif %}
36
- </div>
37
- {% endif %}
38
- {% endfor %}
39
- </div>
40
- <div class="input-group mb-3">
41
- <button type="submit" name="{{ function }}" id="{{ function }}" class="form-control" style="background-color: #a5cece;">{{format_name(function)}} </button>
42
-
43
- </div>
44
-
45
- </form>
46
- </div>
47
- </div>
48
- </div>
49
- {% endif %}
50
- {% endfor %}
51
- </div>
52
- <div class="accordion accordion-flush" id="accordionActions" >
53
- <div class="accordion-item">
54
- <h4 class="accordion-header">
55
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#hidden">
56
- Hidden functions
57
- </button>
58
- </h4>
59
- </div>
60
- <div id="hidden" class="accordion-collapse collapse" data-bs-parent="#accordionActions">
61
- <div class="accordion-body">
62
- {% set hidden_instrument = hidden.get(instrument, []) %}
63
- {% for function in hidden_instrument %}
64
- <div>
65
- {{ function }} <a href="{{ url_for('control.remove_hidden', instrument=instrument, function=function) }}"><i class="bi bi-eye-fill"></i></a>
66
- </div>
67
- {% endfor %}
68
- </div>
69
- </div>
70
- </div>
71
-
72
- <script>
73
- const saveOrderUrl = `{{ url_for('control.save_order', instrument=instrument) }}`;
74
- const buttonIds = {{ session['card_order'][instrument] | tojson }};
75
- </script>
76
- <script src="{{ url_for('static', filename='js/sortable_card.js') }}"></script>
77
- <script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
78
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/control/templates/control/controllers_home.html DELETED
@@ -1,55 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Devices{% endblock %}
3
- {% block body %}
4
- <div class="row">
5
- {% if defined_variables %}
6
- {% for instrument in defined_variables %}
7
- <div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
8
- <div class="bg-white rounded shadow-sm position-relative">
9
- {% if deck %}
10
- {# <a href="{{ url_for('control.disconnect', instrument=instrument) }}" class="stretched-link controller-card" style="float: right;color: red; position: relative;">Disconnect <i class="bi bi-x-square"></i></a>#}
11
- <div class="p-4 controller-card">
12
- <h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument.split(".")[1]}}</a></h5>
13
- </div>
14
- {% else %}
15
- <div class="p-4 controller-card">
16
- <h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a></h5>
17
- </div>
18
- {% endif %}
19
- </div>
20
- </div>
21
- {% endfor %}
22
- <div class="d-flex mb-3">
23
- <a href="{{ url_for('control.download_proxy', filetype='proxy') }}" class="btn btn-outline-primary">
24
- <i class="bi bi-download"></i> Download remote control script
25
- </a>
26
- </div>
27
- {% if not deck %}
28
- <div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
29
- <div class="bg-white rounded shadow-sm position-relative">
30
- <div class="p-4 controller-card" >
31
- {% if deck %}
32
- {# todo disconnect for imported deck #}
33
- {# <h5> <a href="{{ url_for("disconnect") }}" class="stretched-link" style="color: orangered">Disconnect deck</a></h5>#}
34
- {% else %}
35
- <h5><a href="{{ url_for('control.new_controller') }}" style="color: orange" class="stretched-link">New connection</a></h5>
36
- {% endif %}
37
- </div>
38
- </div>
39
- </div>
40
- {% endif %}
41
- {% else %}
42
- <div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
43
- <div class="bg-white rounded shadow-sm position-relative">
44
- <div class="p-4 controller-card" >
45
- {% if deck %}
46
- <h5><a data-bs-toggle="modal" href="#importModal" class="stretched-link"><i class="bi bi-folder-plus"></i> Import deck </a></h5>
47
- {% else %}
48
- <h5><a href="{{ url_for('control.new_controller') }}" style="color: orange" class="stretched-link">New connection</a></h5>
49
- {% endif %}
50
- </div>
51
- </div>
52
- </div>
53
- {% endif %}
54
- </div>
55
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/control/templates/control/controllers_new.html DELETED
@@ -1,89 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | New devices{% endblock %}
3
-
4
- {% block body %}
5
- <div class="row">
6
- <div class="col-xl-4 col-lg-4 col-md-6 mb-4 ">
7
- <h5>Available Python API</h5>
8
- <hr>
9
- {% for instrument in api_variables %}
10
- <div class="bg-white rounded shadow-sm position-relative">
11
- <h5 class="p-3 controller-card">
12
- <a href="{{ url_for('control.new_controller', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a>
13
- </h5>
14
- </div>
15
- {% endfor %}
16
- <div class="bg-white rounded shadow-sm position-relative">
17
- <h5 class="p-3 controller-card">
18
- <a data-bs-toggle="modal" href="#importAPI" class="stretched-link"><i class="bi bi-folder-plus"></i> Import API</a>
19
- </h5>
20
- </div>
21
- </div>
22
- <div class="col-xl-5 col-lg-5 col-md-6 mb-4 ">
23
- {% if device %}
24
- <h5>Connecting</h5><hr>
25
- <form role="form" method='POST' name="init" action="{{ url_for('control.new_controller', instrument=instrument) }}">
26
- <div class="form-group">
27
- <div class="input-group mb-3">
28
- <span class="input-group-text" >Name this device</span>
29
- <input class="form-control" type="text" id="device_name" name="device_name" aria-labelledby="nameHelpBlock" placeholder="e.g. {{device.__name__}}_1" >
30
- <div id="nameHelpBlock" class="form-text">
31
- Name your instrument, avoid names that are defined on the right
32
- </div>
33
- </div>
34
- {% for arg in device.__init__.__annotations__ %}
35
- {% if not arg == "return" %}
36
- <div class="input-group mb-3">
37
- <span class="input-group-text" >{{arg}}</span>
38
- <input class="form-control" type="text" id="{{arg}}" name="{{arg}}"
39
- placeholder="{{device.__init__.__annotations__[arg].__name__}}"
40
- value="{{args.parameters[arg].default if not args.parameters[arg].default.__name__ == '_empty' else ''}}">
41
- {% if device.__init__.__annotations__[arg].__module__ is not in ["builtins", "typing"] %}
42
- <a role="button" href="{{ url_for('control.new_controller', instrument=device.__init__.__annotations__[arg].__name__) }}" class="btn btn-secondary">initialize {{device.__init__.__annotations__[arg].__name__}} first</a>
43
- {% endif %}
44
- </div>
45
- {% endif %}
46
- {% endfor %}
47
- <button type="submit" class="btn btn-dark">Connect</button>
48
- </div>
49
- </form>
50
- {% endif %}
51
- </div>
52
- <div class="col-xl-3 col-lg-3 col-md-6 mb-4">
53
- <h5>Defined Instruments</h5><hr>
54
- {% if defined_variables %}
55
- <ul class="list-group">
56
- {% for instrument in defined_variables %}
57
- <li class="list-group-item">{{instrument}}</li>
58
- {% endfor %}
59
- </ul>
60
- {% endif %}
61
- </div>
62
- </div>
63
-
64
-
65
- <div class="modal fade" id="importAPI" tabindex="-1" aria-labelledby="importModal" aria-hidden="true" >
66
- <div class="modal-dialog">
67
- <div class="modal-content">
68
- <div class="modal-header">
69
- <h1 class="modal-title fs-5" id="importModal">Import API by file path</h1>
70
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
71
- </div>
72
- <form method="POST" action="{{ url_for('control.import_api') }}" enctype="multipart/form-data">
73
- <div class="modal-body">
74
- <h5>input manually</h5>
75
- <div class="input-group mb-3">
76
- <label class="input-group-text" for="filepath">File Path:</label>
77
- <input type="text" class="form-control" name="filepath" id="filepath">
78
- </div>
79
- <div class="modal-footer">
80
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
81
- <button type="submit" class="btn btn-primary"> Save </button>
82
- </div>
83
- </div>
84
- </form>
85
- </div>
86
- </div>
87
- </div>
88
-
89
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/database/__init__.py DELETED
File without changes
ivoryos/routes/database/database.py DELETED
@@ -1,306 +0,0 @@
1
- from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
2
- from flask_login import login_required
3
-
4
- from ivoryos.utils.db_models import Script, db, WorkflowRun, WorkflowStep
5
- from ivoryos.utils.utils import get_script_file, post_script_file
6
-
7
- database = Blueprint('database', __name__, template_folder='templates/database')
8
-
9
-
10
-
11
- @database.route("/database/scripts/edit/<script_name>")
12
- @login_required
13
- def edit_workflow(script_name:str):
14
- """
15
- .. :quickref: Database; load workflow script to canvas
16
-
17
- load the selected workflow to the design canvas
18
-
19
- .. http:get:: /database/scripts/edit/<script_name>
20
-
21
- :param script_name: script name
22
- :type script_name: str
23
- :status 302: redirect to :http:get:`/ivoryos/design/script/`
24
- """
25
- row = Script.query.get(script_name)
26
- script = Script(**row.as_dict())
27
- post_script_file(script)
28
- pseudo_name = session.get("pseudo_deck", "")
29
- off_line = current_app.config["OFF_LINE"]
30
- if off_line and pseudo_name and not script.deck == pseudo_name:
31
- flash(f"Choose the deck with name {script.deck}")
32
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
33
- return jsonify({
34
- "script": script.as_dict(),
35
- "python_script": script.compile(),
36
- })
37
- return redirect(url_for('design.experiment_builder'))
38
-
39
-
40
- @database.route("/database/scripts/delete/<script_name>")
41
- @login_required
42
- def delete_workflow(script_name: str):
43
- """
44
- .. :quickref: Database; delete workflow
45
-
46
- delete workflow from database
47
-
48
- .. http:get:: /database/scripts/delete/<script_name>
49
-
50
- :param script_name: workflow name
51
- :type script_name: str
52
- :status 302: redirect to :http:get:`/ivoryos/database/scripts/`
53
-
54
- """
55
- Script.query.filter(Script.name == script_name).delete()
56
- db.session.commit()
57
- return redirect(url_for('database.load_from_database'))
58
-
59
-
60
- @database.route("/database/scripts/save")
61
- @login_required
62
- def publish():
63
- """
64
- .. :quickref: Database; save workflow to database
65
-
66
- save workflow to database
67
-
68
- .. http:get:: /database/scripts/save
69
-
70
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
71
- """
72
- script = get_script_file()
73
- if not script.name or not script.deck:
74
- flash("Deck cannot be empty, try to re-submit deck configuration on the left panel")
75
- row = Script.query.get(script.name)
76
- if row and row.status == "finalized":
77
- flash("This is a protected script, use save as to rename.")
78
- elif row and not session['user'] == row.author:
79
- flash("You are not the author, use save as to rename.")
80
- else:
81
- db.session.merge(script)
82
- db.session.commit()
83
- flash("Saved!")
84
- return redirect(url_for('design.experiment_builder'))
85
-
86
-
87
- @database.route("/database/scripts/finalize")
88
- @login_required
89
- def finalize():
90
- """
91
- .. :quickref: Database; finalize the workflow
92
-
93
- [protected workflow] prevent saving edited workflow to the same workflow name
94
-
95
- .. http:get:: /finalize
96
-
97
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
98
-
99
- """
100
- script = get_script_file()
101
- script.finalize()
102
- if script.name:
103
- db.session.merge(script)
104
- db.session.commit()
105
- post_script_file(script)
106
- return redirect(url_for('design.experiment_builder'))
107
-
108
-
109
- @database.route("/database/scripts/", strict_slashes=False)
110
- @database.route("/database/scripts/<deck_name>")
111
- @login_required
112
- def load_from_database(deck_name=None):
113
- """
114
- .. :quickref: Database; database page
115
-
116
- backend control through http requests
117
-
118
- .. http:get:: /database/scripts/<deck_name>
119
-
120
- :param deck_name: filter for deck name
121
- :type deck_name: str
122
-
123
- """
124
- session.pop('edit_action', None) # reset cache
125
- query = Script.query
126
- search_term = request.args.get("keyword", None)
127
- if search_term:
128
- query = query.filter(Script.name.like(f'%{search_term}%'))
129
- if deck_name is None:
130
- temp = Script.query.with_entities(Script.deck).distinct().all()
131
- deck_list = [i[0] for i in temp]
132
- else:
133
- query = query.filter(Script.deck == deck_name)
134
- deck_list = ["ALL"]
135
- page = request.args.get('page', default=1, type=int)
136
- per_page = 10
137
-
138
- scripts = query.paginate(page=page, per_page=per_page, error_out=False)
139
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
140
- scripts = query.all()
141
- script_names = [script.name for script in scripts]
142
- return jsonify({
143
- "workflows": script_names,
144
- })
145
- else:
146
- # return HTML
147
- return render_template("scripts_database.html", scripts=scripts, deck_list=deck_list, deck_name=deck_name)
148
-
149
-
150
- @database.route("/database/scripts/rename", methods=['POST'])
151
- @login_required
152
- def edit_run_name():
153
- """
154
- .. :quickref: Database; edit workflow name
155
-
156
- edit the name of the current workflow, won't save to the database
157
-
158
- .. http:post:: database/scripts/rename
159
-
160
- : form run_name: new workflow name
161
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
162
-
163
- """
164
- if request.method == "POST":
165
- run_name = request.form.get("run_name")
166
- exist_script = Script.query.get(run_name)
167
- if not exist_script:
168
- script = get_script_file()
169
- script.save_as(run_name)
170
- post_script_file(script)
171
- else:
172
- flash("Script name is already exist in database")
173
- return redirect(url_for("design.experiment_builder"))
174
-
175
-
176
- @database.route("/database/scripts/save_as", methods=['POST'])
177
- @login_required
178
- def save_as():
179
- """
180
- .. :quickref: Database; save the run name as
181
-
182
- save the current workflow script as
183
-
184
- .. http:post:: /database/scripts/save_as
185
-
186
- : form run_name: new workflow name
187
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
188
-
189
- """
190
- if request.method == "POST":
191
- run_name = request.form.get("run_name")
192
- register_workflow = request.form.get("register_workflow")
193
- exist_script = Script.query.get(run_name)
194
- if not exist_script:
195
- script = get_script_file()
196
- script.save_as(run_name)
197
- script.registered = register_workflow == "on"
198
- script.author = session.get('user')
199
- post_script_file(script)
200
- publish()
201
- else:
202
- flash("Script name is already exist in database")
203
- return redirect(url_for("design.experiment_builder"))
204
-
205
-
206
- # -----------------------------------------------------------
207
- # ------------------ Workflow logs -----------------------
208
- # -----------------------------------------------------------
209
- @database.route('/database/workflows/')
210
- def list_workflows():
211
- """
212
- .. :quickref: Database; list all workflow logs
213
-
214
- list all workflow logs
215
-
216
- .. http:get:: /database/workflows/
217
-
218
- """
219
- query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
220
- search_term = request.args.get("keyword", None)
221
- if search_term:
222
- query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
223
- page = request.args.get('page', default=1, type=int)
224
- per_page = 10
225
-
226
- workflows = query.paginate(page=page, per_page=per_page, error_out=False)
227
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
228
- workflows = query.all()
229
- workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
230
- return jsonify({
231
- "workflow_data": workflow_data,
232
- })
233
- else:
234
- return render_template('workflow_database.html', workflows=workflows)
235
-
236
-
237
- @database.route("/database/workflows/<int:workflow_id>")
238
- def get_workflow_steps(workflow_id:int):
239
- """
240
- .. :quickref: Database; list all workflow logs
241
-
242
- list all workflow logs
243
-
244
- .. http:get:: /database/workflows/<int:workflow_id>
245
-
246
- """
247
- workflow = db.session.get(WorkflowRun, workflow_id)
248
- steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
249
-
250
- # Use full objects for template rendering
251
- grouped = {
252
- "prep": [],
253
- "script": {},
254
- "cleanup": [],
255
- }
256
-
257
- # Use dicts for JSON response
258
- grouped_json = {
259
- "prep": [],
260
- "script": {},
261
- "cleanup": [],
262
- }
263
-
264
- for step in steps:
265
- step_dict = step.as_dict()
266
-
267
- if step.phase == "prep":
268
- grouped["prep"].append(step)
269
- grouped_json["prep"].append(step_dict)
270
-
271
- elif step.phase == "script":
272
- grouped["script"].setdefault(step.repeat_index, []).append(step)
273
- grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
274
-
275
- elif step.phase == "cleanup" or step.method_name == "stop":
276
- grouped["cleanup"].append(step)
277
- grouped_json["cleanup"].append(step_dict)
278
-
279
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
280
- return jsonify({
281
- "workflow_info": workflow.as_dict(),
282
- "steps": grouped_json,
283
- })
284
- else:
285
- return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
286
-
287
-
288
- @database.route("/database/workflows/delete/<int:workflow_id>")
289
- @login_required
290
- def delete_workflow_data(workflow_id: int):
291
- """
292
- .. :quickref: Database; delete experiment data from database
293
-
294
- delete workflow data from database
295
-
296
- .. http:get:: /database/workflows/delete/<int:workflow_id>
297
-
298
- :param workflow_id: workflow id
299
- :type workflow_id: int
300
- :status 302: redirect to :http:get:`/ivoryos/database/workflows/`
301
-
302
- """
303
- run = WorkflowRun.query.get(workflow_id)
304
- db.session.delete(run)
305
- db.session.commit()
306
- return redirect(url_for('database.list_workflows'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/database/templates/database/scripts_database.html DELETED
@@ -1,83 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}IvoryOS | Design Database{% endblock %}
4
- {% block body %}
5
- <div class="database-filter">
6
- {% for deck_name in deck_list %}
7
- {% if deck_name == "ALL" %}<a class="btn btn-secondary" href="{{url_for('database.load_from_database')}}">Back</a>
8
- {% else %}<a class="btn btn-secondary" href="{{url_for('database.load_from_database',deck_name=deck_name)}}">{{deck_name}}</a>
9
- {% endif %}
10
- {% endfor %}
11
-
12
- <form id="search" style="display: inline-block;float: right;" action="{{url_for('database.load_from_database',deck_name=deck_name)}}" method="GET">
13
- <div class="input-group">
14
- <div class="form-outline">
15
- <input type="search" name="keyword" id="keyword" class="form-control" placeholder="Search workflows...">
16
- </div>
17
- <button type="submit" class="btn btn-primary">
18
- <i class="bi bi-search"></i>
19
- </button>
20
- </div>
21
- </form>
22
- </div>
23
-
24
- <table class="table table-hover" id="workflowLibrary">
25
- <thead>
26
- <tr>
27
- <th scope="col">Workflow name</th>
28
- <th scope="col">Deck </th>
29
- <th scope="col">Editing</th>
30
- <th scope="col">Time created</th>
31
- <th scope="col">Last modified</th>
32
- <th scope="col">Author</th>
33
- {# <th scope="col">Registered</th>#}
34
- <th scope="col"></th>
35
- </tr>
36
- </thead>
37
- <tbody>
38
- {% for script in scripts %}
39
- <tr>
40
- <td><a href="{{ url_for('database.edit_workflow', script_name=script.name) }}">{{ script.name }}</a></td>
41
- <td>{{ script.deck }}</td>
42
- <td>{{ script.status }}</td>
43
- <td>{{ script.time_created }}</td>
44
- <td>{{ script.last_modified }}</td>
45
- <td>{{ script.author }}</td>
46
- {# <td>{{ workflow.registered }}</td>#}
47
- <td>
48
- {#not workflow.status == "finalized" or#}
49
- {% if session['user'] == 'admin' or session['user'] == script.author %}
50
- <a href="{{ url_for('database.delete_workflow', script_name=script.name) }}">delete</a>
51
- {% else %}
52
- <a class="disabled-link">delete</a>
53
- {% endif %}
54
- <td>
55
- </tr>
56
- {% endfor %}
57
- </tbody>
58
- </table>
59
-
60
- {# paging#}
61
- <div class="pagination justify-content-center">
62
- <div class="page-item {{ 'disabled' if not scripts.has_prev else '' }}">
63
- <a class="page-link" href="{{ url_for('database.load_from_database', page=scripts.prev_num) }}">Previous</a>
64
- </div>
65
-
66
- {% for num in scripts.iter_pages() %}
67
- {% if num %}
68
- <div class="page-item {{ 'active' if num == scripts.page else '' }}">
69
- <a class="page-link" href="{{ url_for('database.load_from_database', page=num) }}">{{ num }}</a>
70
- </div>
71
- {% else %}
72
- <div class="page-item disabled">
73
- <span class="page-link">…</span>
74
- </div>
75
- {% endif %}
76
- {% endfor %}
77
-
78
- <div class="page-item {{ 'disabled' if not scripts.has_next else '' }}">
79
- <a class="page-link" href="{{ url_for('database.load_from_database', page=scripts.next_num) }}">Next</a>
80
- </div>
81
- </div>
82
-
83
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/database/templates/database/step_card.html DELETED
@@ -1,7 +0,0 @@
1
- <div class="card mb-2 {{ 'border-danger text-danger bg-light' if step.run_error else 'border-secondary' }}">
2
- <div class="card-body p-2">
3
- <strong>{{ step.method_name }}</strong>
4
- <small>Start: {{ step.start_time }}</small>
5
- <small>End: {{ step.end_time }}</small>
6
- </div>
7
- </div>
 
 
 
 
 
 
 
 
ivoryos/routes/database/templates/database/workflow_database.html DELETED
@@ -1,103 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}IvoryOS | Design Database{% endblock %}
4
- {% block body %}
5
- <div class="div">
6
- <form id="search" style="display: inline-block;float: right;" action="{{url_for('database.list_workflows',deck_name=deck_name)}}" method="GET">
7
- <div class="input-group">
8
- <div class="form-outline">
9
- <input type="search" name="keyword" id="keyword" class="form-control" placeholder="Search workflows...">
10
- </div>
11
- <button type="submit" class="btn btn-primary">
12
- <i class="bi bi-search"></i>
13
- </button>
14
- </div>
15
- </form>
16
- </div>
17
-
18
- <table class="table table-hover" id="workflowResultLibrary">
19
- <thead>
20
- <tr>
21
- <th scope="col">Workflow name</th>
22
- <th scope="col">Workflow ID</th>
23
- <th scope="col">Start time</th>
24
- <th scope="col">End time</th>
25
- <th scope="col">Data</th>
26
- </tr>
27
- </thead>
28
- <tbody>
29
- {% for workflow in workflows %}
30
- <tr>
31
- <td><a href="{{ url_for('database.get_workflow_steps', workflow_id=workflow.id) }}">{{ workflow.name }}</a></td>
32
- <td>{{ workflow.id }}</td>
33
- <td>{{ workflow.start_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.start_time else '' }}</td>
34
- <td>{{ workflow.end_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.end_time else '' }}</td>
35
-
36
- <td>
37
- {% if workflow.data_path %}
38
- <a href="{{ url_for('design.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
39
- {% endif %}
40
- </td>
41
- <td>
42
- {% if session['user'] == 'admin' or session['user'] == workflow.author %}
43
- <a href="{{ url_for('database.delete_workflow_data', workflow_id=workflow.id) }}">delete</a>
44
- {% else %}
45
- <a class="disabled-link">delete</a>
46
- {% endif %}
47
- </td>
48
- </tr>
49
- {% endfor %}
50
- </tbody>
51
- </table>
52
-
53
- {# paging#}
54
- <div class="pagination justify-content-center">
55
- <div class="page-item {{ 'disabled' if not workflows.has_prev else '' }}">
56
- <a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.prev_num) }}">Previous</a>
57
- </div>
58
-
59
- {% for num in workflows.iter_pages() %}
60
- {% if num %}
61
- <div class="page-item {{ 'active' if num == workflows.page else '' }}">
62
- <a class="page-link" href="{{ url_for('database.list_workflows', page=num) }}">{{ num }}</a>
63
- </div>
64
- {% else %}
65
- <div class="page-item disabled">
66
- <span class="page-link">…</span>
67
- </div>
68
- {% endif %}
69
- {% endfor %}
70
-
71
- <div class="page-item {{ 'disabled' if not workflows.has_next else '' }}">
72
- <a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.next_num) }}">Next</a>
73
- </div>
74
- </div>
75
-
76
- <div id="steps-container"></div>
77
-
78
- <script>
79
- function showSteps(workflowId) {
80
- fetch(`/workflow_steps/${workflowId}`)
81
- .then(response => response.json())
82
- .then(data => {
83
- const container = document.getElementById('steps-container');
84
- container.innerHTML = ''; // Clear previous content
85
- const stepsList = document.createElement('ul');
86
-
87
- data.steps.forEach(step => {
88
- const li = document.createElement('li');
89
- li.innerHTML = `
90
- <strong>Step: </strong> ${step.method_name} <br>
91
- <strong>Start Time:</strong> ${step.start_time} <br>
92
- <strong>End Time:</strong> ${step.end_time} <br>
93
- <strong>Human Intervention:</strong> ${step.run_error ? 'Yes' : 'No'}
94
- `;
95
- stepsList.appendChild(li);
96
- });
97
-
98
- container.appendChild(stepsList);
99
- });
100
- }
101
- </script>
102
-
103
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/database/templates/database/workflow_view.html DELETED
@@ -1,130 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}IvoryOS | Experiment Results{% endblock %}
4
-
5
- {% block body %}
6
- <style>
7
- .vis-time-axis .vis-text.vis-minor,
8
- .vis-time-axis .vis-text.vis-major {
9
- color: #666;
10
- }
11
- .vis-item.stop {
12
- background-color: red;
13
- color: white;
14
- border: none;
15
- font-weight: bold;
16
- }
17
- </style>
18
-
19
- <div id="timeline"></div>
20
-
21
- <script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
22
- <link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
23
-
24
- <h1>Experiment Step View</h1>
25
-
26
- <div id="visualization"></div>
27
-
28
- <script type="text/javascript">
29
- var container = document.getElementById('visualization');
30
-
31
- const items = [
32
- {% if grouped.prep %}
33
- {
34
- id: 'prep',
35
- content: 'Prep Phase',
36
- start: '{{ grouped.prep[0].start_time }}',
37
- end: '{{ grouped.prep[-1].end_time }}',
38
- className: 'prep',
39
- group: 'prep'
40
- },
41
- {% endif %}
42
-
43
- {% for repeat_index, step_list in grouped.script.items()|sort %}
44
- {
45
- id: 'iter{{ repeat_index }}',
46
- content: 'Iteration {{ repeat_index }}',
47
- start: '{{ step_list[0].start_time }}',
48
- end: '{{ step_list[-1].end_time }}',
49
- className: 'script',
50
- group: 'iter{{ repeat_index }}'
51
- },
52
- {% for step in step_list %}
53
- {% if step.method_name == "stop" %}
54
- {
55
- id: 'stop-{{ step.id }}',
56
- content: '🛑 Stop',
57
- start: '{{ step.start_time }}',
58
- type: 'point',
59
- className: 'stop',
60
- group: 'iter{{ repeat_index }}'
61
- },
62
- {% endif %}
63
- {% endfor %}
64
- {% endfor %}
65
-
66
- {% if grouped.cleanup %}
67
- {
68
- id: 'cleanup',
69
- content: 'Cleanup Phase',
70
- start: '{{ grouped.cleanup[0].start_time }}',
71
- end: '{{ grouped.cleanup[-1].end_time }}',
72
- className: 'cleanup',
73
- group: 'cleanup'
74
-
75
- },
76
- {% endif %}
77
- ];
78
-
79
- const groups = [
80
- {% if grouped.prep %}{ id: 'prep', content: 'Prep' },{% endif %}
81
- {% for repeat_index in grouped.script.keys()|sort %}{ id: 'iter{{ repeat_index }}', content: 'Iteration {{ repeat_index }}' },{% endfor %}
82
- {% if grouped.cleanup %}{ id: 'cleanup', content: 'Cleanup' },{% endif %}
83
- ];
84
-
85
- var options = {
86
- clickToUse: true,
87
- stack: false, // important to keep point within group row
88
- horizontalScroll: true,
89
- zoomKey: 'ctrlKey'
90
- };
91
-
92
- // Initialize your timeline with the sorted groups
93
- const timeline = new vis.Timeline(container, items, groups, options);
94
-
95
- timeline.on('select', function (props) {
96
- const id = props.items[0];
97
- if (id && id.startsWith('iter')) {
98
- const card = document.getElementById('card-' + id);
99
- if (card) {
100
- const yOffset = -80;
101
- const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
102
- window.scrollTo({ top: y, behavior: 'smooth' });
103
- }
104
- }
105
- });
106
- </script>
107
-
108
- <h2>Workflow: {{ workflow.name }}</h2>
109
-
110
- {% if grouped.prep %}
111
- <h4 class="mt-4">Prep Phase</h4>
112
- {% for step in grouped.prep %}
113
- {% include "step_card.html" %}
114
- {% endfor %}
115
- {% endif %}
116
-
117
- {% for repeat_index, step_list in grouped.script.items()|sort %}
118
- <h4 class="mt-4" id="card-iter{{ repeat_index }}">Iteration {{ repeat_index }}</h4>
119
- {% for step in step_list %}
120
- {% include "step_card.html" %}
121
- {% endfor %}
122
- {% endfor %}
123
-
124
- {% if grouped.cleanup %}
125
- <h4 class="mt-4">Cleanup Phase</h4>
126
- {% for step in grouped.cleanup %}
127
- {% include "step_card.html" %}
128
- {% endfor %}
129
- {% endif %}
130
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/design/__init__.py DELETED
File without changes
ivoryos/routes/design/design.py DELETED
@@ -1,792 +0,0 @@
1
- import csv
2
- import json
3
- import os
4
- import pickle
5
- import sys
6
- import time
7
- import eventlet
8
- eventlet.monkey_patch()
9
-
10
- from flask import Blueprint, redirect, url_for, flash, jsonify, send_file, request, render_template, session, \
11
- current_app, g
12
- from flask_login import login_required
13
- from flask_socketio import SocketIO
14
- from werkzeug.utils import secure_filename
15
-
16
- from ivoryos.utils import utils
17
- from ivoryos.utils.global_config import GlobalConfig
18
- from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
19
- create_form_from_action, create_all_builtin_forms
20
- from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
21
- from ivoryos.utils.script_runner import ScriptRunner
22
- # from ivoryos.utils.utils import load_workflows
23
-
24
-
25
- socketio = SocketIO(
26
- async_mode="eventlet",
27
- cors_allowed_origins="*"
28
- )
29
- design = Blueprint('design', __name__, template_folder='templates/design')
30
-
31
- global_config = GlobalConfig()
32
- runner = ScriptRunner()
33
-
34
- def abort_pending():
35
- runner.abort_pending()
36
- socketio.emit('log', {'message': "aborted pending iterations"})
37
-
38
- def abort_current():
39
- runner.stop_execution()
40
- socketio.emit('log', {'message': "stopped next task"})
41
-
42
- def pause():
43
- runner.retry = False
44
- msg = runner.toggle_pause()
45
- socketio.emit('log', {'message': msg})
46
- return msg
47
-
48
- def retry():
49
- runner.retry = True
50
- msg = runner.toggle_pause()
51
- socketio.emit('log', {'message': msg})
52
-
53
-
54
- # ---- Socket.IO Event Handlers ----
55
-
56
- @socketio.on('abort_pending')
57
- def handle_abort_pending():
58
- abort_pending()
59
-
60
- @socketio.on('abort_current')
61
- def handle_abort_current():
62
- abort_current()
63
-
64
- @socketio.on('pause')
65
- def handle_pause():
66
- pause()
67
-
68
- @socketio.on('retry')
69
- def handle_retry():
70
- retry()
71
-
72
-
73
- @socketio.on('connect')
74
- def handle_abort_action():
75
- # Fetch log messages from local file
76
- filename = os.path.join(current_app.config["OUTPUT_FOLDER"], current_app.config["LOGGERS_PATH"])
77
- with open(filename, 'r') as log_file:
78
- log_history = log_file.readlines()
79
- for message in log_history[-10:]:
80
- socketio.emit('log', {'message': message})
81
-
82
-
83
- @design.route("/design/script/", methods=['GET', 'POST'])
84
- @design.route("/design/script/<instrument>/", methods=['GET', 'POST'])
85
- @login_required
86
- def experiment_builder(instrument=None):
87
- """
88
- .. :quickref: Workflow Design; Build experiment workflow
89
-
90
- **Experiment Builder**
91
-
92
- This route allows users to build and edit experiment workflows. Users can interact with available instruments,
93
- define variables, and manage experiment scripts.
94
-
95
- .. http:get:: /design/script
96
-
97
- Load the experiment builder interface.
98
-
99
- :param instrument: The specific instrument for which to load functions and forms.
100
- :type instrument: str
101
- :status 200: Experiment builder loaded successfully.
102
-
103
- .. http:post:: /design/script
104
-
105
- Submit form data to add or modify actions in the experiment script.
106
-
107
- **Adding action to canvas**
108
-
109
- :form return: (optional) The name of the function or method to add to the script.
110
- :form dynamic: depend on the selected instrument and its metadata.
111
-
112
- :status 200: Action added or modified successfully.
113
- :status 400: Validation errors in submitted form data.
114
- :status 302: Toggles autofill or redirects to refresh the page.
115
-
116
- **Toggle auto parameter name fill**:
117
-
118
- :status 200: autofill toggled successfully
119
-
120
- """
121
- deck = global_config.deck
122
- script = utils.get_script_file()
123
- # load_workflows(script)
124
- # registered_workflows = global_config.registered_workflows
125
-
126
- if deck and script.deck is None:
127
- script.deck = os.path.splitext(os.path.basename(deck.__file__))[
128
- 0] if deck.__name__ == "__main__" else deck.__name__
129
- # script.sort_actions()
130
-
131
- pseudo_deck_name = session.get('pseudo_deck', '')
132
- pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pseudo_deck_name)
133
- off_line = current_app.config["OFF_LINE"]
134
- enable_llm = current_app.config["ENABLE_LLM"]
135
- autofill = session.get('autofill')
136
-
137
- # autofill is not allowed for prep and cleanup
138
- autofill = autofill if script.editing_type == "script" else False
139
- forms = None
140
- pseudo_deck = utils.load_deck(pseudo_deck_path) if off_line and pseudo_deck_name else None
141
- if off_line and pseudo_deck is None:
142
- flash("Choose available deck below.")
143
-
144
- deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
145
-
146
- functions = {}
147
- if deck:
148
- deck_variables = list(global_config.deck_snapshot.keys())
149
- # deck_variables.insert(0, "registered_workflows")
150
- deck_variables.insert(0, "flow_control")
151
-
152
- else:
153
- deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
154
- deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
155
- edit_action_info = session.get("edit_action")
156
- if edit_action_info:
157
- forms = create_form_from_action(edit_action_info, script=script)
158
- elif instrument:
159
- # if instrument in ['if', 'while', 'variable', 'wait', 'repeat']:
160
- # forms = create_builtin_form(instrument, script=script)
161
- if instrument == 'flow_control':
162
- forms = create_all_builtin_forms(script=script)
163
- # elif instrument == 'registered_workflows':
164
- # functions = utils._inspect_class(registered_workflows)
165
- # # forms = create_workflow_forms(script=script)
166
- # forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
167
- elif instrument in global_config.defined_variables.keys():
168
- _object = global_config.defined_variables.get(instrument)
169
- functions = utils._inspect_class(_object)
170
- forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
171
- else:
172
- if deck:
173
- functions = global_config.deck_snapshot.get(instrument, {})
174
- elif pseudo_deck:
175
- functions = pseudo_deck.get(instrument, {})
176
- forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
177
- if request.method == 'POST' and "hidden_name" in request.form:
178
- # all_kwargs = request.form.copy()
179
- method_name = request.form.get("hidden_name", None)
180
- # if method_name is not None:
181
- form = forms.get(method_name)
182
- insert_position = request.form.get("drop_target_id", None)
183
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
184
- if form and form.validate_on_submit():
185
- function_name = kwargs.pop("hidden_name")
186
- save_data = kwargs.pop('return', '')
187
-
188
- primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
189
-
190
- script.eval_list(kwargs, primitive_arg_types)
191
- kwargs = script.validate_variables(kwargs)
192
- action = {"instrument": instrument, "action": function_name,
193
- "args": kwargs,
194
- "return": save_data,
195
- 'arg_types': primitive_arg_types}
196
- script.add_action(action=action, insert_position=insert_position)
197
- else:
198
- flash(form.errors)
199
-
200
- elif request.method == 'POST' and "builtin_name" in request.form:
201
- function_name = request.form.get("builtin_name")
202
- form = forms.get(function_name)
203
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
204
- insert_position = request.form.get("drop_target_id", None)
205
-
206
- if form.validate_on_submit():
207
- # print(kwargs)
208
- logic_type = kwargs.pop('builtin_name')
209
- if 'variable' in kwargs:
210
- try:
211
- script.add_variable(insert_position=insert_position, **kwargs)
212
- except ValueError:
213
- flash("Invalid variable type")
214
- else:
215
- script.add_logic_action(logic_type=logic_type, insert_position=insert_position, **kwargs)
216
- else:
217
- flash(form.errors)
218
- elif request.method == 'POST' and "workflow_name" in request.form:
219
- workflow_name = request.form.get("workflow_name")
220
- form = forms.get(workflow_name)
221
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
222
- insert_position = request.form.get("drop_target_id", None)
223
-
224
- if form.validate_on_submit():
225
- # workflow_name = kwargs.pop('workflow_name')
226
- save_data = kwargs.pop('return', '')
227
-
228
- primitive_arg_types = utils.get_arg_type(kwargs, functions[workflow_name])
229
-
230
- script.eval_list(kwargs, primitive_arg_types)
231
- kwargs = script.validate_variables(kwargs)
232
- action = {"instrument": instrument, "action": workflow_name,
233
- "args": kwargs,
234
- "return": save_data,
235
- 'arg_types': primitive_arg_types}
236
- script.add_action(action=action, insert_position=insert_position)
237
- script.add_workflow(**kwargs, insert_position=insert_position)
238
- else:
239
- flash(form.errors)
240
-
241
- # toggle autofill, autofill doesn't apply to control flow ops
242
- elif request.method == 'POST' and "autofill" in request.form:
243
- autofill = not autofill
244
- session['autofill'] = autofill
245
- if not instrument == 'flow_control':
246
- forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
247
-
248
- utils.post_script_file(script)
249
-
250
- exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
251
- session['python_code'] = exec_string
252
-
253
- design_buttons = create_action_button(script)
254
- return render_template('experiment_builder.html', off_line=off_line, instrument=instrument, history=deck_list,
255
- script=script, defined_variables=deck_variables,
256
- local_variables=global_config.defined_variables,
257
- forms=forms, buttons=design_buttons, format_name=format_name,
258
- use_llm=enable_llm)
259
-
260
-
261
- @design.route("/design/generate_code", methods=['POST'])
262
- @login_required
263
- def generate_code():
264
- """
265
- .. :quickref: Text to Code; Generate code from user input and update the design canvas.
266
-
267
- .. http:post:: /design/generate_code
268
-
269
- :form prompt: user's prompt
270
- :status 200: and then redirects to :http:get:`/experiment/build`
271
- :status 400: failed to initialize the AI agent redirects to :http:get:`/design/script`
272
-
273
- """
274
- agent = global_config.agent
275
- enable_llm = current_app.config["ENABLE_LLM"]
276
- instrument = request.form.get("instrument")
277
-
278
- if request.method == 'POST' and "clear" in request.form:
279
- session['prompt'][instrument] = ''
280
- if request.method == 'POST' and "gen" in request.form:
281
- prompt = request.form.get("prompt")
282
- session['prompt'][instrument] = prompt
283
- # sdl_module = utils.parse_functions(find_instrument_by_name(f'deck.{instrument}'), doc_string=True)
284
- sdl_module = global_config.deck_snapshot.get(instrument, {})
285
- empty_script = Script(author=session.get('user'))
286
- if enable_llm and agent is None:
287
- try:
288
- model = current_app.config["LLM_MODEL"]
289
- server = current_app.config["LLM_SERVER"]
290
- module = current_app.config["MODULE"]
291
- from ivoryos.utils.llm_agent import LlmAgent
292
- agent = LlmAgent(host=server, model=model, output_path=os.path.dirname(os.path.abspath(module)))
293
- except Exception as e:
294
- flash(e.__str__())
295
- return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True)), 400
296
- action_list = agent.generate_code(sdl_module, prompt)
297
- for action in action_list:
298
- action['instrument'] = instrument
299
- action['return'] = ''
300
- if "args" not in action:
301
- action['args'] = {}
302
- if "arg_types" not in action:
303
- action['arg_types'] = {}
304
- empty_script.add_action(action)
305
- utils.post_script_file(empty_script)
306
- return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True))
307
-
308
-
309
- @design.route("/design/campaign", methods=['GET', 'POST'])
310
- @login_required
311
- def experiment_run():
312
- """
313
- .. :quickref: Workflow Execution; Execute/iterate the workflow
314
-
315
- .. http:get:: /design/campaign
316
-
317
- Compile the workflow and load the experiment execution interface.
318
-
319
- .. http:post:: /design/campaign
320
-
321
- Start workflow execution
322
-
323
- """
324
- deck = global_config.deck
325
- script = utils.get_script_file()
326
-
327
- # script.sort_actions() # handled in update list
328
- off_line = current_app.config["OFF_LINE"]
329
- deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
330
- # if not off_line and deck is None:
331
- # # print("loading deck")
332
- # module = current_app.config.get('MODULE', '')
333
- # deck = sys.modules[module] if module else None
334
- # script.deck = os.path.splitext(os.path.basename(deck.__file__))[0]
335
- design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
336
- config_preview = []
337
- config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
338
- try:
339
- # todo
340
- exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
341
- # exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
342
- # print(exec_string)
343
- except Exception as e:
344
- flash(e.__str__())
345
- # handle api request
346
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
347
- return jsonify({"error": e.__str__()})
348
- else:
349
- return redirect(url_for("design.experiment_builder"))
350
-
351
- config_file = request.args.get("filename")
352
- config = []
353
- if config_file:
354
- session['config_file'] = config_file
355
- filename = session.get("config_file")
356
- if filename:
357
- # config_preview = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
358
- config = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
359
- config_preview = config[1:]
360
- arg_type = config.pop(0) # first entry is types
361
- try:
362
- for key, func_str in exec_string.items():
363
- exec(func_str)
364
- line_collection = script.convert_to_lines(exec_string)
365
-
366
- except Exception:
367
- flash(f"Please check {key} syntax!!")
368
- return redirect(url_for("design.experiment_builder"))
369
- # runner.globals_dict.update(globals())
370
- run_name = script.name if script.name else "untitled"
371
-
372
- dismiss = session.get("dismiss", None)
373
- script = utils.get_script_file()
374
- no_deck_warning = False
375
-
376
- _, return_list = script.config_return()
377
- config_list, config_type_list = script.config("script")
378
- # config = script.config("script")
379
- data_list = os.listdir(current_app.config['DATA_FOLDER'])
380
- data_list.remove(".gitkeep") if ".gitkeep" in data_list else data_list
381
- if deck is None:
382
- no_deck_warning = True
383
- flash(f"No deck is found, import {script.deck}")
384
- elif script.deck:
385
- is_deck_match = script.deck == deck.__name__ or script.deck == \
386
- os.path.splitext(os.path.basename(deck.__file__))[0]
387
- if not is_deck_match:
388
- flash(f"This script is not compatible with current deck, import {script.deck}")
389
- if request.method == "POST":
390
- bo_args = None
391
- compiled = False
392
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
393
- payload_json = request.get_json()
394
- compiled = True
395
- if "kwargs" in payload_json:
396
- config = payload_json["kwargs"]
397
- elif "parameters" in payload_json:
398
- bo_args = payload_json
399
- repeat = payload_json.pop("repeat", None)
400
- else:
401
- if "bo" in request.form:
402
- bo_args = request.form.to_dict()
403
- if "online-config" in request.form:
404
- config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
405
- repeat = request.form.get('repeat', None)
406
-
407
- try:
408
- datapath = current_app.config["DATA_FOLDER"]
409
- run_name = script.validate_function_name(run_name)
410
- runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
411
- logger=g.logger, socketio=g.socketio, repeat_count=repeat,
412
- output_path=datapath, compiled=compiled,
413
- current_app=current_app._get_current_object()
414
- )
415
- if utils.check_config_duplicate(config):
416
- flash(f"WARNING: Duplicate in config entries.")
417
- except Exception as e:
418
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
419
- return jsonify({"error": e.__str__()})
420
- else:
421
- flash(e)
422
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
423
- # wait to get a workflow ID
424
- while not global_config.runner_status:
425
- time.sleep(1)
426
- return jsonify({"status": "task started", "task_id": global_config.runner_status.get("id")})
427
- else:
428
- return render_template('experiment_run.html', script=script.script_dict, filename=filename,
429
- dot_py=exec_string, line_collection=line_collection,
430
- return_list=return_list, config_list=config_list, config_file_list=config_file_list,
431
- config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
432
- no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons,
433
- history=deck_list, pause_status=runner.pause_status())
434
-
435
-
436
- @design.route("/design/script/toggle/<stype>")
437
- @login_required
438
- def toggle_script_type(stype=None):
439
- """
440
- .. :quickref: Workflow Design; toggle the experimental phase for design canvas.
441
-
442
- .. http:get:: /design/script/toggle/<stype>
443
-
444
- :status 200: and then redirects to :http:get:`/design/script`
445
-
446
- """
447
- script = utils.get_script_file()
448
- script.editing_type = stype
449
- utils.post_script_file(script)
450
- return redirect(url_for('design.experiment_builder'))
451
-
452
-
453
- @design.route("/updateList", methods=['POST'])
454
- @login_required
455
- def update_list():
456
- order = request.form['order']
457
- script = utils.get_script_file()
458
- script.currently_editing_order = order.split(",", len(script.currently_editing_script))
459
- script.sort_actions()
460
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
461
- utils.post_script_file(script)
462
- session['python_code'] = exec_string
463
-
464
- return jsonify({'success': True})
465
-
466
-
467
- @design.route("/toggle_show_code", methods=["POST"])
468
- def toggle_show_code():
469
- session["show_code"] = not session.get("show_code", False)
470
- return redirect(request.referrer or url_for("design.experiment_builder"))
471
-
472
-
473
- # --------------------handle all the import/export and download/upload--------------------------
474
- @design.route("/design/clear")
475
- @login_required
476
- def clear():
477
- """
478
- .. :quickref: Workflow Design; clear the design canvas.
479
-
480
- .. http:get:: /design/clear
481
-
482
- :form prompt: user's prompt
483
- :status 200: clear canvas and then redirects to :http:get:`/design/script`
484
- """
485
- deck = global_config.deck
486
- pseudo_name = session.get("pseudo_deck", "")
487
- if deck:
488
- deck_name = os.path.splitext(os.path.basename(deck.__file__))[
489
- 0] if deck.__name__ == "__main__" else deck.__name__
490
- elif pseudo_name:
491
- deck_name = pseudo_name
492
- else:
493
- deck_name = ''
494
- script = Script(deck=deck_name, author=session.get('username'))
495
- utils.post_script_file(script)
496
- return redirect(url_for("design.experiment_builder"))
497
-
498
-
499
- @design.route("/design/import/pseudo", methods=['POST'])
500
- @login_required
501
- def import_pseudo():
502
- """
503
- .. :quickref: Workflow Design; Import pseudo deck from deck history
504
-
505
- .. http:post:: /design/import/pseudo
506
-
507
- :form pkl_name: pseudo deck name
508
- :status 302: load pseudo deck and then redirects to :http:get:`/design/script`
509
- """
510
- pkl_name = request.form.get('pkl_name')
511
- script = utils.get_script_file()
512
- session['pseudo_deck'] = pkl_name
513
-
514
- if script.deck is None or script.isEmpty():
515
- script.deck = pkl_name.split('.')[0]
516
- utils.post_script_file(script)
517
- elif script.deck and not script.deck == pkl_name.split('.')[0]:
518
- flash(f"Choose the deck with name {script.deck}")
519
- return redirect(url_for("design.experiment_builder"))
520
-
521
-
522
- @design.route('/design/uploads', methods=['POST'])
523
- @login_required
524
- def upload():
525
- """
526
- .. :quickref: Workflow Execution; upload a workflow config file (.CSV)
527
-
528
- .. http:post:: /design/uploads
529
-
530
- :form file: workflow CSV config file
531
- :status 302: save csv file and then redirects to :http:get:`/design/campaign`
532
- """
533
- if request.method == "POST":
534
- f = request.files['file']
535
- if 'file' not in request.files:
536
- flash('No file part')
537
- if f.filename.split('.')[-1] == "csv":
538
- filename = secure_filename(f.filename)
539
- f.save(os.path.join(current_app.config['CSV_FOLDER'], filename))
540
- session['config_file'] = filename
541
- return redirect(url_for("design.experiment_run"))
542
- else:
543
- flash("Config file is in csv format")
544
- return redirect(url_for("design.experiment_run"))
545
-
546
-
547
- @design.route('/design/workflow/download/<filename>')
548
- @login_required
549
- def download_results(filename):
550
- """
551
- .. :quickref: Workflow Design; download a workflow data file
552
-
553
- .. http:get:: /design/workflow/download/<filename>
554
-
555
- """
556
- filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
557
- return send_file(os.path.abspath(filepath), as_attachment=True)
558
-
559
-
560
- @design.route('/design/load_json', methods=['POST'])
561
- @login_required
562
- def load_json():
563
- """
564
- .. :quickref: Workflow Design Ext; upload a workflow design file (.JSON)
565
-
566
- .. http:post:: /load_json
567
-
568
- :form file: workflow design JSON file
569
- :status 302: load pseudo deck and then redirects to :http:get:`/design/script`
570
- """
571
- if request.method == "POST":
572
- f = request.files['file']
573
- if 'file' not in request.files:
574
- flash('No file part')
575
- if f.filename.endswith("json"):
576
- script_dict = json.load(f)
577
- utils.post_script_file(script_dict, is_dict=True)
578
- else:
579
- flash("Script file need to be JSON file")
580
- return redirect(url_for("design.experiment_builder"))
581
-
582
-
583
- @design.route('/design/script/download/<filetype>')
584
- @login_required
585
- def download(filetype):
586
- """
587
- .. :quickref: Workflow Design Ext; download a workflow design file
588
-
589
- .. http:get:: /design/script/download/<filetype>
590
-
591
- """
592
- script = utils.get_script_file()
593
- run_name = script.name if script.name else "untitled"
594
- if filetype == "configure":
595
- filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}_config.csv")
596
- with open(filepath, 'w', newline='') as f:
597
- writer = csv.writer(f)
598
- cfg, cfg_types = script.config("script")
599
- writer.writerow(cfg)
600
- writer.writerow(list(cfg_types.values()))
601
- elif filetype == "script":
602
- script.sort_actions()
603
- json_object = json.dumps(script.as_dict())
604
- filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}.json")
605
- with open(filepath, "w") as outfile:
606
- outfile.write(json_object)
607
- elif filetype == "python":
608
- filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
609
- else:
610
- return "Unsupported file type", 400
611
- return send_file(os.path.abspath(filepath), as_attachment=True)
612
-
613
-
614
- @design.route("/design/step/edit/<uuid>", methods=['GET', 'POST'])
615
- @login_required
616
- def edit_action(uuid: str):
617
- """
618
- .. :quickref: Workflow Design; edit parameters of an action step on canvas
619
-
620
- .. http:get:: /design/step/edit/<uuid>
621
-
622
- Load parameter form of an action step
623
-
624
- .. http:post:: /design/step/edit/<uuid>
625
-
626
- :param uuid: The step's uuid
627
- :type uuid: str
628
-
629
- :form dynamic form: workflow step dynamic inputs
630
- :status 302: save changes and then redirects to :http:get:`/design/script`
631
- """
632
- script = utils.get_script_file()
633
- action = script.find_by_uuid(uuid)
634
- session['edit_action'] = action
635
-
636
- if request.method == "POST" and action is not None:
637
- forms = create_form_from_action(action, script=script)
638
- if "back" not in request.form:
639
- kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
640
- # print(kwargs)
641
- if forms and forms.validate_on_submit():
642
- save_as = kwargs.pop('return', '')
643
- kwargs = script.validate_variables(kwargs)
644
- # try:
645
- script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as)
646
- # except Exception as e:
647
- else:
648
- flash(forms.errors)
649
- session.pop('edit_action')
650
- return redirect(url_for('design.experiment_builder'))
651
-
652
-
653
- @design.route("/design/step/delete/<id>")
654
- @login_required
655
- def delete_action(id: int):
656
- """
657
- .. :quickref: Workflow Design; delete an action step on canvas
658
-
659
- .. http:get:: /design/step/delete/<id>
660
-
661
- :param id: The step number id
662
- :type id: int
663
-
664
- :status 302: save changes and then redirects to :http:get:`/design/script`
665
- """
666
- back = request.referrer
667
- script = utils.get_script_file()
668
- script.delete_action(id)
669
- utils.post_script_file(script)
670
- return redirect(back)
671
-
672
-
673
- @design.route("/design/step/duplicate/<id>")
674
- @login_required
675
- def duplicate_action(id: int):
676
- """
677
- .. :quickref: Workflow Design; duplicate an action step on canvas
678
-
679
- .. http:get:: /design/step/duplicate/<id>
680
-
681
- :param id: The step number id
682
- :type id: int
683
-
684
- :status 302: save changes and then redirects to :http:get:`/design/script`
685
- """
686
- back = request.referrer
687
- script = utils.get_script_file()
688
- script.duplicate_action(id)
689
- utils.post_script_file(script)
690
- return redirect(back)
691
-
692
-
693
- # ---- HTTP API Endpoints ----
694
-
695
- @design.route("/api/runner/status", methods=["GET"])
696
- def runner_status():
697
- """
698
- .. :quickref: Workflow Design; get the execution status
699
-
700
- .. http:get:: /api/runner/status
701
-
702
- :status 200: status
703
- """
704
- runner_busy = global_config.runner_lock.locked()
705
- status = {"busy": runner_busy}
706
- task_status = global_config.runner_status
707
- current_step = {}
708
- # print(task_status)
709
- if task_status is not None:
710
- task_type = task_status["type"]
711
- task_id = task_status["id"]
712
- if task_type == "task":
713
- step = SingleStep.query.get(task_id)
714
- current_step = step.as_dict()
715
- if task_type == "workflow":
716
- workflow = WorkflowRun.query.get(task_id)
717
- if workflow is not None:
718
- latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(WorkflowStep.start_time.desc()).first()
719
- if latest_step is not None:
720
- current_step = latest_step.as_dict()
721
- status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
722
- status["current_task"] = current_step
723
- return jsonify(status), 200
724
-
725
-
726
-
727
- @design.route("/api/runner/abort_pending", methods=["POST"])
728
- def api_abort_pending():
729
- """
730
- .. :quickref: Workflow Design; abort pending action(s) during execution
731
-
732
- .. http:get:: /api/runner/abort_pending
733
-
734
- :status 200: {"status": "ok"}
735
- """
736
- abort_pending()
737
- return jsonify({"status": "ok"}), 200
738
-
739
- @design.route("/api/runner/abort_current", methods=["POST"])
740
- def api_abort_current():
741
- """
742
- .. :quickref: Workflow Design; abort right after current action during execution
743
-
744
- .. http:get:: /api/runner/abort_current
745
-
746
- :status 200: {"status": "ok"}
747
- """
748
- abort_current()
749
- return jsonify({"status": "ok"}), 200
750
-
751
- @design.route("/api/runner/pause", methods=["POST"])
752
- def api_pause():
753
- """
754
- .. :quickref: Workflow Design; pause during execution
755
-
756
- .. http:get:: /api/runner/pause
757
-
758
- :status 200: {"status": "ok"}
759
- """
760
- msg = pause()
761
- return jsonify({"status": "ok", "pause_status": msg}), 200
762
-
763
- @design.route("/api/runner/retry", methods=["POST"])
764
- def api_retry():
765
- """
766
- .. :quickref: Workflow Design; retry when error occur during execution
767
-
768
- .. http:get:: /api/runner/retry
769
-
770
- :status 200: {"status": "ok"}
771
- """
772
- retry()
773
- return jsonify({"status": "ok, retrying failed step"}), 200
774
-
775
-
776
- @design.route("/api/design/submit", methods=["POST"])
777
- def submit_script():
778
- """
779
- .. :quickref: Workflow Design; submit script
780
-
781
- .. http:get:: /api/design/submit
782
-
783
- :status 200: {"status": "ok"}
784
- """
785
- deck = global_config.deck
786
- deck_name = os.path.splitext(os.path.basename(deck.__file__))[0] if deck.__name__ == "__main__" else deck.__name__
787
- script = Script(author=session.get('user'), deck=deck_name)
788
- script_collection = request.get_json()
789
- script.python_script = script_collection
790
- # todo check script format
791
- utils.post_script_file(script)
792
- return jsonify({"status": "ok"}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/design/templates/design/experiment_builder.html DELETED
@@ -1,521 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Design{% endblock %}
3
-
4
- {% block body %}
5
- {# overlay block for text-to-code gen #}
6
- <style>
7
- .code-overlay {
8
- position: absolute;
9
- top: 0;
10
- right: -50%;
11
- height: 100%;
12
- width: 50%;
13
- z-index: 100;
14
- background: #f8f9fa;
15
- box-shadow: -2px 0 6px rgba(0, 0, 0, 0.1);
16
- transition: right 0.3s ease;
17
- overflow-y: auto;
18
- }
19
- .code-overlay.show {
20
- right: 0;
21
- }
22
- </style>
23
- <div id="overlay" class="overlay">
24
- <div>
25
- <h3 id="overlay-text">Generating design, please wait...</h3>
26
- <div class="spinner-border" role="status"></div>
27
- </div>
28
- </div>
29
-
30
- <div class="row">
31
- <div class="col-md-3 scroll-column" >
32
-
33
- {# select deck if this is online#}
34
- {% if off_line %}
35
- <form id="select-deck" method="POST" action="{{ url_for('design.import_pseudo') }}" enctype="multipart/form-data">
36
- <div class="input-group mb-3">
37
- {# <label for="pkl_name" class="form-label">Choose/Change deck:</label>#}
38
- <select class="form-select" name="pkl_name" id="pkl_name" required onchange="document.getElementById('select-deck').submit();">
39
- <option {{ '' if 'pseudo_deck' in session else 'selected' }} disabled hidden style="overflow-wrap: break-word;" name="pkl_name" id="pkl_name" value=""> -- choose deck --</option>
40
- {% for connection in history %}
41
- <option {{ 'selected' if session['pseudo_deck']==connection else '' }} style="overflow-wrap: break-word;" name="pkl_name" id="pkl_name" value="{{connection}}">{{connection.split('.')[0]}}</option>
42
- {% endfor %}
43
- </select>
44
- </div>
45
- </form>
46
- <hr>
47
- {% endif %}
48
-
49
- {# edit action #}
50
- {% if session["edit_action"] %}
51
- {% with action = session["edit_action"] %}
52
- <h5> {{ format_name(action['action']) }} </h5>
53
- <form role="form" method='POST' name="{{instrument}}" action="{{ url_for('design.edit_action', uuid=session["edit_action"]['uuid']) }}">
54
- {% if not action['args'] == None %}
55
- <div class="form-group">
56
- {% if not action['args'].__class__.__name__ == 'dict' %}
57
- <div class="input-group mb-3">
58
- <label class="input-group-text">{{ action['action'] }}</label>
59
- <input class="form-control" type="text" id="arg" name="arg" placeholder="{{ action['arg_types']}}" value="{{ action['args'] }}" aria-labelledby="variableHelpBlock">
60
- </div>
61
- {% else %}
62
- {# {% for arg in action['args'] %}#}
63
- {# <div class="input-group mb-3">#}
64
- {# <label class="input-group-text">{{ format_name(arg) }}</label>#}
65
- {# <input class="form-control" type="text" id="{{ arg }}" name="{{ arg }}" placeholder="{{ action['arg_types'][arg] }}" value="{{ action['args'][arg] }}" aria-labelledby="variableHelpBlock">#}
66
- {# </div>#}
67
- {# {% endfor %}#}
68
- {# <div class="input-group mb-3">#}
69
- {# <label class="input-group-text">Save Output?</label>#}
70
- {# <input class="form-control" type="text" id="return" name="return" value="{{ action['return'] }}" aria-labelledby="variableHelpBlock">#}
71
- {# </div>#}
72
- {{ forms.hidden_tag() }}
73
- {% for field in forms %}
74
- {% if field.type not in ['CSRFTokenField'] %}
75
- <div class="input-group mb-3">
76
- <label class="input-group-text">{{ field.label.text }}</label>
77
- {{ field(class="form-control") }}
78
- <div class="form-text">{{ field.description }} </div>
79
- </div>
80
- {% endif %}
81
- {% endfor %}
82
- {% endif %}
83
- </div>
84
- {% endif %}
85
- <button class="btn btn-primary" type="submit">Save</button>
86
- <button class="btn btn-primary" type="submit" name="back" id="back" value="back">Back</button>
87
- </form>
88
- {% endwith %}
89
-
90
-
91
- {% elif instrument %}
92
- <div>
93
- <div class="d-flex justify-content-between align-items-center " style="margin-bottom: 1vh;margin-top: 1vh;">
94
- <a class="btn btn-primary" role="button" type="button" href="{{url_for('design.experiment_builder')}}"><i class="bi bi-arrow-return-left"></i></a>
95
- {{ format_name(instrument) }}
96
- </div>
97
-
98
- {% if script.editing_type == "script" %}
99
- {# Auto Fill Toggle #}
100
- <div class="d-flex justify-content-between align-items-center " style="margin-bottom: 1vh;margin-top: 1vh;">
101
- <div></div>
102
- <form role="form" method='POST' name="autoFill" id="autoFill">
103
- <div class="form-check form-switch">
104
- <input type="hidden" id="autofill" name="autofill" value="temp_value">
105
- <input class="form-check-input" type="checkbox" id="autoFillCheck" name="autoFillCheck" onchange="document.getElementById('autoFill').submit();"
106
- value="temp_value"
107
- {{ "checked" if session["autofill"] else "" }}>
108
- <label class="form-check-label" for="autoFillCheck">Auto fill</label>
109
- </div>
110
- <button type="submit" class="btn btn-default" style="display: none;">Auto fill </button>
111
- </form>
112
- </div>
113
- {% endif %}
114
-
115
- {# according for instrument #}
116
- <div class="accordion accordion-flush" id="accordionActions" >
117
- {% if use_llm and not instrument == "flow_control" %}
118
- <div class="accordion-item text-to-code">
119
- <h2 class="accordion-header">
120
- <button class="accordion-button text-to-code" type="button" data-bs-toggle="collapse" data-bs-target="#text-to-code" aria-expanded="false" aria-controls="collapseExample">
121
- Text-to-Code
122
- </button>
123
- </h2>
124
- <div id="text-to-code" class="accordion-collapse collapse show" data-bs-parent="#accordionActions">
125
- <div class="accordion-body">
126
- <form role="form" method='POST' name="generate" id="generate" action="{{url_for('design.generate_code')}}">
127
- <input type="hidden" id="instrument" name="instrument" value="{{instrument}}">
128
- <textarea class="form-control" id="prompt" name="prompt" rows="6" aria-describedby="promptHelpBlock">{{ session['prompt'][instrument] if instrument in session['prompt'] else '' }}</textarea>
129
- <div id="promptHelpBlock" class="form-text">
130
- This will overwrite current design.
131
- </div>
132
- <!-- <button type="submit" class="btn btn-dark" id="clear" name="clear" onclick="submitForm('generate')">Clear</button>-->
133
- <button type="submit" class="btn btn-dark" id="gen" name="gen">Generate</button>
134
- </form>
135
- </div>
136
- </div>
137
- </div>
138
- {% endif %}
139
-
140
- {% for name, form in forms.items() %}
141
- <div class="accordion-item design-control" draggable="true">
142
- <h2 class="accordion-header">
143
- <button class="accordion-button collapsed draggable-action"
144
- type="button" data-bs-toggle="collapse"
145
- data-bs-target="#{{name}}" aria-expanded="false"
146
- aria-controls="collapseExample"
147
- data-action="{{ name }}">
148
- {{ format_name(name) }}
149
- </button>
150
- </h2>
151
- <div id="{{name}}" class="accordion-collapse collapse" data-bs-parent="#accordionActions">
152
- <div class="accordion-body">
153
- <form role="form" method='POST' name="add" id="add-{{name}}">
154
- <div class="form-group">
155
- {{ form.hidden_tag() }}
156
- {% for field in form %}
157
- {% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
158
- <div class="input-group mb-3">
159
- <label class="input-group-text">{{ field.label.text }}</label>
160
- {% if field.type == "SubmitField" %}
161
- {{ field(class="btn btn-dark") }}
162
- {% elif field.type == "BooleanField" %}
163
- {{ field(class="form-check-input") }}
164
- {% elif field.type == "FlexibleEnumField" %}
165
- <input type="text" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.data }}"
166
- list="{{ field.id }}_options" placeholder="{{ field.render_kw.placeholder if field.render_kw and field.render_kw.placeholder }}"
167
- class="form-control">
168
- <datalist id="{{ field.id }}_options">
169
- {% for key in field.choices %}
170
- <option value="{{ key }}">{{ key }}</option>
171
- {% endfor %}
172
- </datalist>
173
-
174
- {% else %}
175
- {{ field(class="form-control") }}
176
- {% endif %}
177
- </div>
178
- {% endif %}
179
- {% endfor %}
180
- </div>
181
- <button type="submit" class="btn btn-dark">Add</button>
182
- {% if 'hidden_name' in form %}
183
- <i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top"
184
- title='{{ form.hidden_name.description or "Docstring is not available" }}'>
185
- </i>
186
- {% else %}
187
- <!-- handle info tooltip for flow control / workflows -->
188
- {% endif %}
189
-
190
- </form>
191
- </div>
192
- </div>
193
- </div>
194
- {% endfor %}
195
- </div>
196
- </div>
197
-
198
-
199
- {# according for all actions #}
200
- {% else %}
201
- <div style="margin-bottom: 4vh;"></div>
202
- <div class="accordion accordion-flush">
203
-
204
- <div class="accordion-item design-control">
205
- <h5 class="accordion-header">
206
- <button class="accordion-button" data-bs-toggle="collapse" data-bs-target="#deck" role="button" aria-expanded="false" aria-controls="collapseExample">
207
- Operations
208
- </button>
209
- </h5>
210
- <div class="accordion-collapse collapse show" id="deck">
211
- <ul class="list-group">
212
- {% for instrument in defined_variables %}
213
- <form role="form" method='GET' name="{{instrument}}" action="{{url_for('design.experiment_builder',instrument=instrument)}}">
214
- <div>
215
- <button class="list-group-item list-group-item-action" type="submit">{{format_name(instrument)}}</button>
216
- </div>
217
- </form>
218
- {% endfor %}
219
- </ul>
220
- </div>
221
- </div>
222
-
223
- {% if local_variables %}
224
- <div class="accordion-item design-control">
225
- <h5 class="accordion-header">
226
- <button class="accordion-button" data-bs-toggle="collapse" data-bs-target="#local" role="button" aria-expanded="false" aria-controls="collapseExample">
227
- Local Operations
228
- </button>
229
- </h5>
230
- <div class="accordion-collapse collapse show" id="local">
231
- <ul class="list-group">
232
- {% for instrument in local_variables %}
233
- <form role="form" method='GET' name="{{instrument}}" action="{{url_for('design.experiment_builder',instrument=instrument)}}">
234
- <div>
235
- <button class="list-group-item list-group-item-action" type="submit" name="device" value="{{instrument}}" >{{instrument}}</button>
236
- </div>
237
- </form>
238
- {% endfor%}
239
- </ul>
240
- </div>
241
- </div>
242
- {% endif %}
243
- </div>
244
- {% endif %}
245
- </div>
246
-
247
- {# canvas #}
248
- <div class="col-md-9 scroll-column">
249
- <div class="d-flex align-items-center ">
250
- {# file dropdown menu #}
251
- <ul class="nav nav-tabs">
252
- <li class="nav-item dropdown">
253
- <a class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">File tools <i class="bi bi-tools"></i></a>
254
- <ul class="dropdown-menu">
255
- <button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#newScriptModal">New</button>
256
- <button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#jsonModal">Import (.json <i class="bi bi-filetype-json"></i>)</button>
257
- <button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#renameModal">Rename</button>
258
- <a role="button" class="dropdown-item {{'disabled' if not script.name or script.status == 'finalized'}}" type="button" href="{{url_for('database.publish')}}">Save</a></li>
259
- <button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#saveasModal">Save as</button>
260
- {% if not script.status == 'finalized' %}
261
- <a role="button" class="dropdown-item" type="button" href="{{url_for('database.finalize')}}">Disable editing</a>
262
- {% endif %}
263
- <a class="dropdown-item" role="button" type="button" href="{{url_for('design.download', filetype='script')}}">Export (.json <i class="bi bi-filetype-json"></i>)</a>
264
- </ul>
265
- </li>
266
-
267
- <li class="nav-item"><a class="nav-link" aria-current="page" data-bs-toggle="collapse" href="#info">Info</a></li>
268
- <li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='prep' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='prep') }}">Prep</a></li>
269
- <li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='script' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='script') }}">Experiment</a></li>
270
- <li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='cleanup' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='cleanup') }}">Clean up</a></li>
271
- </ul>
272
- <form method="POST" action="{{ url_for('design.toggle_show_code') }}" class="ms-3">
273
- <div class="form-check form-switch">
274
- <input class="form-check-input" type="checkbox" id="showPythonCodeSwitch" name="show_code"
275
- onchange="this.form.submit()" {% if session.get('show_code') %}checked{% endif %}>
276
- <label class="form-check-label" for="showPythonCodeSwitch">Show Python Code</label>
277
- </div>
278
- </form>
279
-
280
- <div class="form-check form-switch ms-auto">
281
- <input class="form-check-input" type="checkbox" id="toggleLineNumbers" onchange="toggleLineNumbers()">
282
- <label class="form-check-label" for="toggleLineNumbers">Show Line Numbers</label>
283
- </div>
284
-
285
- </div>
286
- <div class="canvas-wrapper position-relative">
287
- <div class="canvas" droppable="true">
288
- <div class="collapse" id="info">
289
- <table class="table script-table">
290
- <tbody>
291
- <tr><th scope="row">Deck Name</th><td>{{script.deck}}</td></tr>
292
- <tr><th scope="row">Script Name</th><td>{{ script.name }}</td></tr>
293
- <tr>
294
- <th scope="row">Editing status <a role="button" data-bs-toggle="popover" data-bs-title="How to use:" data-bs-content="You can choose to disable editing, so the script is finalized and cannot be edited. Use save as to rename the script"><i class="bi bi-info-circle"></i></a></th>
295
- <td>{{script.status}}</td>
296
- </tr>
297
- <tr>
298
- <th scope="row">Output Values <a role="button" data-bs-toggle="popover" data-bs-title="How to use:" data-bs-content="This will be your output data. If the return data is not a value, it will save as None is the result file"><i class="bi bi-info-circle"></i></a></th>
299
- <td>
300
- {% for i in script.config_return()[1] %}
301
- <input type="checkbox">{{i}}
302
- {% endfor %}
303
- </td>
304
- </tr>
305
- <tr>
306
- <th scope="row">Config Variables <a role="button" data-bs-toggle="popover" data-bs-title="How to use:" data-bs-content="This shows variables you want to configure later using .csv file"><i class="bi bi-info-circle"></i></a></th>
307
- <td>
308
- <ul>
309
- {% for i in script.config("script")[0] %}
310
- <li>{{i}}</li>
311
- {% endfor %}
312
- </ul>
313
- </td>
314
- </tr>
315
- </tbody>
316
- </table>
317
- </div>
318
-
319
- <div class="list-group" id="list" style="margin-top: 20px">
320
- <ul class="reorder">
321
- {% for button in buttons %}
322
- <li id="{{ button['id'] }}" style="list-style-type: none;">
323
- <span class="line-number d-none">{{ button['id'] }}.</span>
324
- <a href="{{ url_for('design.edit_action', uuid=button['uuid']) }}" type="button" class="btn btn-light" style="{{ button['style'] }}">{{ button['label'] }}</a>
325
- {% if not button["instrument"] in ["if","while","repeat"] %}
326
- <a href="{{ url_for('design.duplicate_action', id=button['id']) }}" type="button" class="btn btn-light"><span class="bi bi-copy"></span></a>
327
- {% endif %}
328
- <a href="{{ url_for('design.delete_action', id=button['id']) }}" type="button" class="btn btn-light"><span class="bi bi-trash"></span></a>
329
- </li>
330
- {% endfor %}
331
- </ul>
332
-
333
- <!-- Python Code Overlay -->
334
- <!-- Right side: Python code (conditionally shown) -->
335
- <!-- Python Code Slide-Over Panel -->
336
- {% if session.get('show_code') %}
337
- <div id="pythonCodeOverlay" class="code-overlay bg-light border-start show">
338
- <div class="overlay-header d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
339
- <strong>Python Code</strong>
340
- <button class="btn btn-sm btn-outline-secondary" onclick="toggleCodeOverlay(false)">
341
- <i class="bi bi-x-lg"></i>
342
- </button>
343
- </div>
344
- <div class="overlay-content p-3">
345
- {% for stype, script in session['python_code'].items() %}
346
- <pre><code class="language-python">{{ script }}</code></pre>
347
- {% endfor %}
348
- <a href="{{ url_for('design.download', filetype='python') }}">Download <i class="bi bi-download"></i></a>
349
- </div>
350
- </div>
351
- {% endif %}
352
- </div>
353
- </div>
354
- <div>
355
- <a class="btn btn-dark {{ 'disabled' if not script.name or script.status == "finalized" else ''}}" href="{{url_for('database.publish')}}">Quick Save</a>
356
- <a class="btn btn-dark " href="{{ url_for('design.experiment_run') }}">Compile and Run</a>
357
- </div>
358
- </div>
359
- </div>
360
-
361
- {# modals #}
362
- <div class="modal fade" id="newScriptModal" tabindex="-1" aria-labelledby="newScriptModalLabel" aria-hidden="true">
363
- <div class="modal-dialog">
364
- <div class="modal-content">
365
- <div class="modal-header">
366
- <h1 class="modal-title fs-5" id="newScriptModalLabel">Save your current editing!</h1>
367
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
368
- </div>
369
- <div class="modal-body">
370
- The current editing won't be saved. Are you sure you want to proceed?
371
- </div>
372
- <div class="modal-footer">
373
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Continue editing </button>
374
- <a role="button" class="btn btn-primary" href="{{url_for('design.clear')}}"> Already saved, clear all </a>
375
- </div>
376
- </div>
377
- </div>
378
- </div>
379
- <div class="modal fade" id="saveasModal" tabindex="-1" aria-labelledby="saveasModal" aria-hidden="true" >
380
- <div class="modal-dialog">
381
- <div class="modal-content">
382
- <div class="modal-header">
383
- <h1 class="modal-title fs-5" id="saveasModal">Save your script as </h1>
384
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
385
- </div>
386
- <form method="POST" name="run_name" action="{{ url_for('database.save_as') }}">
387
- <div class="modal-body">
388
- <div class="input-group mb-3">
389
- <label class="input-group-text" for="run_name">Run Name</label>
390
- <input class="form-control" type="text" name="run_name" id="run_name" placeholder="{{script['name']}}" required="required">
391
- </div>
392
- <div class="form-check form-switch">
393
- <input class="form-check-input" type="checkbox" name="register_workflow" id="register_workflow">
394
- <label class="input-group-label" for="register_workflow">Register this workflow</label>
395
- </div>
396
- </div>
397
- <div class="modal-footer">
398
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
399
- <button type="submit" class="btn btn-primary"> Save </button>
400
- </div>
401
- </form>
402
- </div>
403
- </div>
404
- </div>
405
- <div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModal" aria-hidden="true" >
406
- <div class="modal-dialog">
407
- <div class="modal-content">
408
- <div class="modal-header">
409
- <h1 class="modal-title fs-5" id="renameModal">Rename your script</h1>
410
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
411
- </div>
412
- <form method="POST" name="run_name" action="{{ url_for('database.edit_run_name') }}">
413
- <div class="modal-body">
414
- <div class="input-group mb-3">
415
- <label class="input-group-text" for="run_name">Run Name</label>
416
- <input class="form-control" type="text" name="run_name" id="run_name" placeholder="{{script['name']}}" required="required">
417
- </div>
418
- </div>
419
- <div class="modal-footer">
420
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
421
- <button type="submit" class="btn btn-primary"> Save </button>
422
- </div>
423
- </form>
424
- </div>
425
- </div>
426
- </div>
427
- <div class="modal fade" id="jsonModal" tabindex="-1" aria-labelledby="jsonModal" aria-hidden="true" >
428
- <div class="modal-dialog">
429
- <div class="modal-content">
430
- <div class="modal-header">
431
- <h1 class="modal-title fs-5" id="jsonModal">Import from JSON</h1>
432
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
433
- </div>
434
- <form method="POST" action="{{ url_for('design.load_json') }}" enctype="multipart/form-data">
435
- <div class="modal-body">
436
- <div class="input-group mb-3">
437
- <input class="form-control" type="file" name="file" required="required">
438
- </div>
439
- </div>
440
- <div class="modal-footer">
441
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
442
- <button type="submit" class="btn btn-primary"> Upload </button>
443
- </div>
444
- </form>
445
- </div>
446
- </div>
447
- </div>
448
- <!-- Bootstrap Modal -->
449
- <div class="modal fade" id="dropModal" tabindex="-1" aria-labelledby="dropModalLabel" aria-hidden="true">
450
- <div class="modal-dialog">
451
- <div class="modal-content">
452
- <div class="modal-header">
453
- <h5 class="modal-title" id="dropModalLabel">Configure Action</h5>
454
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
455
- </div>
456
- <div class="modal-body">
457
- <p>Drop Position ID: <strong id="modalDropTarget"></strong></p>
458
-
459
- <!-- Form will be dynamically inserted here -->
460
- <div id="modalFormFields"></div>
461
- </div>
462
- <div class="modal-footer">
463
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
464
- </div>
465
- </div>
466
- </div>
467
- </div>
468
-
469
-
470
- {% if instrument and use_llm %}
471
- <script>
472
- const buttonIds = {{ ['generate'] | tojson }};
473
- </script>
474
- <script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
475
- {% endif %}
476
-
477
- <script>
478
- const updateListUrl = "{{ url_for('design.update_list') }}";
479
-
480
- // Toggle visibility of line numbers
481
- function toggleLineNumbers(save = true) {
482
- const show = document.getElementById('toggleLineNumbers').checked;
483
- document.querySelectorAll('.line-number').forEach(el => {
484
- el.classList.toggle('d-none', !show);
485
- });
486
-
487
- if (save) {
488
- localStorage.setItem('showLineNumbers', show ? 'true' : 'false');
489
- }
490
- }
491
-
492
- function toggleCodeOverlay() {
493
- const overlay = document.getElementById("pythonCodeOverlay");
494
- const toggleBtn = document.getElementById("codeToggleBtn");
495
- overlay.classList.toggle("show");
496
-
497
- // Change arrow icon
498
- const icon = toggleBtn.querySelector("i");
499
- icon.classList.toggle("bi-chevron-left");
500
- icon.classList.toggle("bi-chevron-right");
501
- }
502
-
503
- // Restore state on page load
504
- document.addEventListener('DOMContentLoaded', () => {
505
- const savedState = localStorage.getItem('showLineNumbers');
506
- const checkbox = document.getElementById('toggleLineNumbers');
507
-
508
- if (savedState === 'true') {
509
- checkbox.checked = true;
510
- }
511
-
512
- toggleLineNumbers(false); // don't overwrite localStorage on load
513
-
514
- checkbox.addEventListener('change', () => toggleLineNumbers());
515
- });
516
- </script>
517
-
518
-
519
- <script src="{{ url_for('static', filename='js/sortable_design.js') }}"></script>
520
-
521
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/design/templates/design/experiment_run.html DELETED
@@ -1,558 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Design execution{% endblock %}
3
-
4
- {% block body %}
5
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.4.1/socket.io.js"></script>
6
-
7
-
8
- {% if no_deck_warning and not dismiss %}
9
- {# auto pop import when there is no deck#}
10
- <script type="text/javascript">
11
- function OpenBootstrapPopup() {
12
- $("#importModal").modal('show');
13
- }
14
- window.onload = function () {
15
- OpenBootstrapPopup();
16
- };
17
- </script>
18
- {% endif %}
19
-
20
- <div class="row">
21
- {% if script['script'] or script['prep'] or script['cleanup'] %}
22
- <div class="col-lg-6 col-sm-12" id="run-panel" style="{{ 'display: none;' if pause_status else '' }}">
23
- <ul class="nav nav-tabs" id="myTabs" role="tablist">
24
- <li class="nav-item" role="presentation">
25
- <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>
26
- </li>
27
- <li class="nav-item" role="presentation">
28
- <a class="nav-link {{ 'disabled' if not config_list else '' }} {{ 'active' if config_list else '' }}" id="tab4-tab" data-bs-toggle="tab" href="#tab4" role="tab" aria-controls="tab4" aria-selected="false">Configuration</a>
29
- </li>
30
- <li class="nav-item" role="presentation">
31
- <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>
32
- </li>
33
- </ul>
34
- <div class="tab-content" id="myTabsContent">
35
- <div class="tab-pane fade {{ 'show active' if not config_list else '' }}" id="tab1" role="tabpanel" aria-labelledby="tab1-tab">
36
- <p><h5>Control panel:</h5></p>
37
- <form role="form" method='POST' name="run" action="{{url_for('design.experiment_run')}}">
38
- <div class="input-group mb-3">
39
- <label class="input-group-text" for="repeat">Repeat for </label>
40
- <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="1">
41
- <label class="input-group-text" for="repeat"> times</label>
42
- </div>
43
- <div class="input-group mb-3">
44
- <button class="form-control" type="submit" class="btn btn-dark">Run</button>
45
- </div>
46
- </form>
47
- </div>
48
-
49
- {#TODO#}
50
- <div class="tab-pane fade {{ 'show active' if config_list else '' }}" id="tab4" role="tabpanel" aria-labelledby="tab4-tab">
51
- <!-- File Management Section -->
52
- <div class="card mb-4">
53
- <div class="card-header d-flex justify-content-between align-items-center">
54
- <h6 class="mb-0"><i class="bi bi-file-earmark-text"></i> Configuration File</h6>
55
- <small class="text-muted">
56
- <a href="{{ url_for('design.download', filetype='configure') }}">
57
- <i class="bi bi-download"></i> Download Empty Template
58
- </a>
59
- </small>
60
- </div>
61
- <div class="card-body">
62
- <div class="row g-3">
63
- <!-- File Selection -->
64
- <div class="col-md-6">
65
- <form name="filenameForm" id="filenameForm" method="GET" action="{{ url_for('design.experiment_run') }}" enctype="multipart/form-data">
66
- <div class="input-group">
67
- <label class="input-group-text"><i class="bi bi-folder2-open"></i></label>
68
- <select class="form-select" name="filename" id="filenameSelect" onchange="document.getElementById('filenameForm').submit();">
69
- <option {{ 'selected' if not filename else '' }} value="">-- Select existing file --</option>
70
- {% for config_file in config_file_list %}
71
- <option {{ 'selected' if filename == config_file else '' }} value="{{ config_file }}">{{ config_file }}</option>
72
- {% endfor %}
73
- </select>
74
- </div>
75
- </form>
76
- </div>
77
-
78
- <!-- File Upload -->
79
- <div class="col-md-6">
80
- <form method="POST" id="loadFile" name="loadFile" action="{{ url_for('design.upload') }}" enctype="multipart/form-data">
81
- <div class="input-group">
82
- <input class="form-control" name="file" type="file" accept=".csv" required="required" onchange="document.getElementById('loadFile').submit();">
83
- </div>
84
- </form>
85
- </div>
86
- </div>
87
- </div>
88
- </div>
89
- <!-- Configuration Table -->
90
-
91
-
92
-
93
- <div class="card mb-4">
94
- <div class="card-header position-relative">
95
- <div class="position-absolute top-50 end-0 translate-middle-y me-3">
96
- <span id="saveStatus" class="badge bg-success" style="display: none;">
97
- <i class="bi bi-check-circle"></i> Auto-saved
98
- </span>
99
- <span id="modifiedStatus" class="badge bg-warning" style="display: none;">
100
- <i class="bi bi-pencil"></i> Modified
101
- </span>
102
- </div>
103
- </div>
104
- <div class="card-body p-0">
105
- <form method="POST" name="online-config" id="online-config" action="{{url_for('design.experiment_run')}}">
106
- <div class="table-responsive">
107
- <table id="dataInputTable" class="table table-striped table-hover mb-0">
108
- <thead class="table-dark">
109
- <tr>
110
- <th style="width: 40px;">#</th>
111
- {% for column in config_list %}
112
- <th>{{ column }}</th>
113
- {% endfor %}
114
- <th></th>
115
- </tr>
116
- </thead>
117
- <tbody id="tableBody">
118
- </tbody>
119
- </table>
120
- </div>
121
- <div class="card-footer">
122
- <div class="d-flex justify-content-between align-items-center">
123
- <div class="d-flex gap-2">
124
- <button type="button" class="btn btn-success" onclick="addRow()">
125
- <i class="bi bi-plus-circle"></i> Add Row
126
- </button>
127
- <button type="button" class="btn btn-warning" onclick="clearAllRows()">
128
- <i class="bi bi-trash"></i> Clear All
129
- </button>
130
- <button type="button" class="btn btn-info" onclick="resetToFile()" id="resetToFileBtn" style="display: none;">
131
- <i class="bi bi-arrow-clockwise"></i> Reset to File
132
- </button>
133
- </div>
134
- <button type="submit" name="online-config" class="btn btn-primary btn-lg">
135
- <i class="bi bi-play-circle"></i> Run
136
- </button>
137
- </div>
138
- </div>
139
- </form>
140
- </div>
141
- <!-- Config Preview (if loaded from file) -->
142
- {% if config_preview %}
143
- <div class="alert alert-info">
144
- <small><i class="bi bi-info-circle"></i> {{ config_preview|length }} rows loaded from {{ filename }}</small>
145
- </div>
146
- {% endif %}
147
- </div>
148
- </div>
149
-
150
- <div class="tab-pane fade" id="tab3" role="tabpanel" aria-labelledby="tab3-tab">
151
- <form method="POST" name="bo" action="{{ url_for('design.experiment_run') }}">
152
- <div class="container py-2">
153
- <!-- Parameters -->
154
- <h6 class="fw-bold mt-2 mb-1">Parameters</h6>
155
- {% for config in config_list %}
156
- <div class="row align-items-center mb-2">
157
- <div class="col-3 col-form-label-sm">
158
- {{ config }}:
159
- </div>
160
- <div class="col-6">
161
- <select class="form-select form-select-sm" id="{{config}}_type" name="{{config}}_type">
162
- <option selected value="range">range</option>
163
- <option value="choice">choice</option>
164
- <option value="fixed">fixed</option>
165
- </select>
166
- </div>
167
- <div class="col-3">
168
- <input type="text" class="form-control form-control-sm" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">
169
- </div>
170
- </div>
171
- {% endfor %}
172
- <!-- Objective -->
173
- <h6 class="fw-bold mt-3 mb-1">Objectives</h6>
174
- {% for objective in return_list %}
175
- <div class="row align-items-center mb-2">
176
- <div class="col-3 col-form-label-sm">
177
- {{ objective }}:
178
- </div>
179
- <div class="col-6">
180
- <select class="form-select form-select-sm" id="{{objective}}_min" name="{{objective}}_min">
181
- <option selected>minimize</option>
182
- <option>maximize</option>
183
- <option>none</option>
184
- </select>
185
- </div>
186
- </div>
187
- {% endfor %}
188
- <h6 class="fw-bold mt-3 mb-1">Budget</h6>
189
- <div class="input-group mb-3">
190
- <label class="input-group-text" for="repeat">Max iteration </label>
191
- <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">
192
- </div>
193
- {% if not no_deck_warning%}
194
- <div class="input-group mb-3">
195
- <button class="form-control" type="submit" name="bo">Run</button>
196
- </div>
197
- {% endif %}
198
- </div>
199
- </form>
200
- </div>
201
- </div>
202
- </div>
203
- {% else %}
204
- <div class="col-lg-6 col-sm-12" id="placeholder-panel">
205
- </div>
206
- {% endif %}
207
-
208
- <div class="col-lg-6 col-sm-12" id="code-panel" style="{{ '' if pause_status else 'display: none;'}}">
209
- <p>
210
- <h5>Progress:</h5>
211
- {% if "prep" in line_collection.keys() %}
212
- {% set stype = "prep" %}
213
- <h6>Preparation:</h6>
214
- {% for code in line_collection["prep"] %}
215
- <pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
216
- {% endfor %}
217
- {% endif %}
218
- {% if "script" in line_collection.keys() %}
219
- {% set stype = "script" %}
220
- <h6>Experiment:</h6>
221
- {% for code in line_collection["script"] %}
222
- <pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
223
- {% endfor %}
224
- {% endif %}
225
- {% if "cleanup" in line_collection.keys() %}
226
- {% set stype = "cleanup" %}
227
- <h6>Cleanup:</h6>
228
- {% for code in line_collection["cleanup"] %}
229
- <pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
230
- {% endfor %}
231
- {% endif %}
232
- </p>
233
- </div>
234
- <div class="col-lg-6 col-sm-12 logging-panel">
235
- <p>
236
- <div class="p d-flex justify-content-between align-items-center">
237
- <h5>Progress:</h5>
238
- <div class="d-flex gap-2 ms-auto">
239
- <button id="pause-resume" class="btn btn-info text-white" data-bs-toggle="tooltip" title="Pause execution">
240
- {% if pause_status %}
241
- <i class="bi bi-play-circle"></i>
242
- {% else %}
243
- <i class="bi bi-pause-circle"></i>
244
- {% endif %}
245
- </button>
246
- <button id="abort-current" class="btn btn-danger text-white" data-bs-toggle="tooltip" title="Stop execution after current step">
247
- <i class="bi bi-stop-circle"></i>
248
- </button>
249
- <button id="abort-pending" class="btn btn-warning text-white" data-bs-toggle="tooltip" title="Stop execution after current iteration">
250
- <i class="bi bi-hourglass-split"></i>
251
- </button>
252
- </div>
253
- </div>
254
- <div class="text-muted mt-2">
255
- <small><strong>Note:</strong> The current step cannot be paused or stopped until it completes. </small>
256
- </div>
257
-
258
- <div class="progress" role="progressbar" aria-label="Animated striped example" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100">
259
- <div id="progress-bar-inner" class="progress-bar progress-bar-striped progress-bar-animated"></div>
260
- </div>
261
- <p><h5>Log:</h5></p>
262
- <div id="logging-panel"></div>
263
- </div>
264
-
265
- </div>
266
-
267
-
268
-
269
-
270
-
271
- <!-- Error Modal -->
272
- <div class="modal fade" id="error-modal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
273
- <div class="modal-dialog">
274
- <div class="modal-content">
275
- <div class="modal-header">
276
- <h5 class="modal-title" id="errorModalLabel">Error Detected</h5>
277
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
278
- </div>
279
- <div class="modal-body">
280
- <p id="error-message">An error has occurred.</p>
281
- <p>Do you want to continue execution or stop?</p>
282
- </div>
283
- <div class="modal-footer">
284
- <button type="button" class="btn btn-primary" id="retry-btn" data-bs-dismiss="modal">Rerun Current Step</button>
285
- <button type="button" class="btn btn-success" id="continue-btn" data-bs-dismiss="modal">Continue</button>
286
- <button type="button" class="btn btn-danger" id="stop-btn" data-bs-dismiss="modal">Stop Execution</button>
287
- </div>
288
- </div>
289
- </div>
290
- </div>
291
-
292
- <script src="{{ url_for('static', filename='js/socket_handler.js') }}"></script>
293
- <script>
294
- var rowCount = 0;
295
- var configColumns = [
296
- {% for column in config_list %}
297
- '{{ column }}'{{ ',' if not loop.last else '' }}
298
- {% endfor %}
299
- ];
300
- var configTypes = {
301
- {% for column, type in config_type_list.items() %}
302
- '{{ column }}': '{{ type }}'{{ ',' if not loop.last else '' }}
303
- {% endfor %}
304
- };
305
-
306
- // State management
307
- var originalFileData = null;
308
- var isModifiedFromFile = false;
309
- var saveTimeout = null;
310
- var lastSavedData = null;
311
-
312
- function addRow(data = null, skipSave = false) {
313
- rowCount++;
314
- var tableBody = document.getElementById("tableBody");
315
- var newRow = tableBody.insertRow(-1);
316
-
317
- // Row number cell
318
- var rowNumCell = newRow.insertCell(-1);
319
- rowNumCell.innerHTML = '<span class="badge bg-secondary">' + rowCount + '</span>';
320
-
321
- // Data cells
322
- configColumns.forEach(function(column, index) {
323
- var cell = newRow.insertCell(-1);
324
- var value = data && data[column] ? data[column] : '';
325
- var placeholder = configTypes[column] || 'value';
326
- cell.innerHTML = '<input type="text" class="form-control form-control-sm" name="' +
327
- column + '[' + rowCount + ']" value="' + value + '" placeholder="' + placeholder +
328
- '" oninput="onInputChange()" onchange="onInputChange()">';
329
- });
330
-
331
- // Action cell
332
- var actionCell = newRow.insertCell(-1);
333
- actionCell.innerHTML = '<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRow(this)" title="Remove row">' +
334
- '<i class="bi bi-trash"></i></button>';
335
-
336
- if (!skipSave) {
337
- markAsModified();
338
- debouncedSave();
339
- }
340
- }
341
-
342
- function removeRow(button) {
343
- var row = button.closest('tr');
344
- row.remove();
345
- updateRowNumbers();
346
- markAsModified();
347
- debouncedSave();
348
- }
349
-
350
- function updateRowNumbers() {
351
- var tableBody = document.getElementById("tableBody");
352
- var rows = tableBody.getElementsByTagName('tr');
353
- for (var i = 0; i < rows.length; i++) {
354
- var badge = rows[i].querySelector('.badge');
355
- if (badge) {
356
- badge.textContent = i + 1;
357
- }
358
- }
359
- }
360
-
361
- function clearAllRows() {
362
- if (confirm('Are you sure you want to clear all rows?')) {
363
- var tableBody = document.getElementById("tableBody");
364
- tableBody.innerHTML = '';
365
- rowCount = 0;
366
- markAsModified();
367
- clearSavedData();
368
- // Add 5 empty rows by default
369
- for (let i = 0; i < 5; i++) {
370
- addRow(null, true);
371
- }
372
- debouncedSave();
373
- }
374
- }
375
-
376
- function resetToFile() {
377
- if (originalFileData && confirm('Reset to original file data? This will lose all manual changes.')) {
378
- loadDataFromSource(originalFileData, false);
379
- isModifiedFromFile = false;
380
- updateStatusIndicators();
381
- debouncedSave();
382
- }
383
- }
384
-
385
- function onInputChange() {
386
- markAsModified();
387
- debouncedSave();
388
- }
389
-
390
- function markAsModified() {
391
- if (originalFileData) {
392
- isModifiedFromFile = true;
393
- updateStatusIndicators();
394
- }
395
- }
396
-
397
- function updateStatusIndicators() {
398
- var modifiedStatus = document.getElementById('modifiedStatus');
399
- var resetBtn = document.getElementById('resetToFileBtn');
400
-
401
- if (isModifiedFromFile && originalFileData) {
402
- modifiedStatus.style.display = 'inline-block';
403
- resetBtn.style.display = 'inline-block';
404
- } else {
405
- modifiedStatus.style.display = 'none';
406
- resetBtn.style.display = 'none';
407
- }
408
- }
409
-
410
- function showSaveStatus() {
411
- var saveStatus = document.getElementById('saveStatus');
412
- saveStatus.style.display = 'inline-block';
413
- setTimeout(function() {
414
- saveStatus.style.display = 'none';
415
- }, 2000);
416
- }
417
-
418
- function debouncedSave() {
419
- clearTimeout(saveTimeout);
420
- saveTimeout = setTimeout(function() {
421
- saveFormData();
422
- showSaveStatus();
423
- }, 1000); // Save 1 second after user stops typing
424
- }
425
-
426
- function saveFormData() {
427
- var formData = getCurrentFormData();
428
- try {
429
- sessionStorage.setItem('configFormData', JSON.stringify(formData));
430
- sessionStorage.setItem('configModified', isModifiedFromFile.toString());
431
- lastSavedData = formData;
432
- } catch (e) {
433
- console.warn('Could not save form data to sessionStorage:', e);
434
- }
435
- }
436
-
437
- function getCurrentFormData() {
438
- var tableBody = document.getElementById("tableBody");
439
- var rows = tableBody.getElementsByTagName('tr');
440
- var data = [];
441
-
442
- for (var i = 0; i < rows.length; i++) {
443
- var inputs = rows[i].getElementsByTagName('input');
444
- var rowData = {};
445
- var hasData = false;
446
-
447
- for (var j = 0; j < inputs.length; j++) {
448
- var input = inputs[j];
449
- var name = input.name;
450
- if (name) {
451
- var columnName = name.substring(0, name.indexOf('['));
452
- rowData[columnName] = input.value;
453
- if (input.value.trim() !== '') {
454
- hasData = true;
455
- }
456
- }
457
- }
458
-
459
- if (hasData) {
460
- data.push(rowData);
461
- }
462
- }
463
-
464
- return data;
465
- }
466
-
467
- function loadSavedData() {
468
- try {
469
- var savedData = sessionStorage.getItem('configFormData');
470
- var savedModified = sessionStorage.getItem('configModified');
471
-
472
- if (savedData) {
473
- var parsedData = JSON.parse(savedData);
474
- isModifiedFromFile = savedModified === 'true';
475
- return parsedData;
476
- }
477
- } catch (e) {
478
- console.warn('Could not load saved form data:', e);
479
- }
480
- return null;
481
- }
482
-
483
- function clearSavedData() {
484
- try {
485
- sessionStorage.removeItem('configFormData');
486
- sessionStorage.removeItem('configModified');
487
- } catch (e) {
488
- console.warn('Could not clear saved data:', e);
489
- }
490
- }
491
-
492
- function loadDataFromSource(data, isFromFile = false) {
493
- // Clear existing rows
494
- var tableBody = document.getElementById("tableBody");
495
- tableBody.innerHTML = '';
496
- rowCount = 0;
497
-
498
- // Add rows with data
499
- data.forEach(function(rowData) {
500
- addRow(rowData, true);
501
- });
502
-
503
- // Add a few empty rows for additional input
504
- for (let i = 0; i < 3; i++) {
505
- addRow(null, true);
506
- }
507
-
508
- if (isFromFile) {
509
- originalFileData = JSON.parse(JSON.stringify(data)); // Deep copy
510
- isModifiedFromFile = false;
511
- clearSavedData(); // Clear saved data when loading from file
512
- }
513
-
514
- updateStatusIndicators();
515
- }
516
-
517
- function loadConfigData() {
518
- // Check for saved form data first
519
- var savedData = loadSavedData();
520
-
521
- {% if config_preview %}
522
- var fileData = {{ config_preview | tojson | safe }};
523
- originalFileData = JSON.parse(JSON.stringify(fileData)); // Deep copy
524
-
525
- if (savedData && savedData.length > 0) {
526
- // Load saved data if available
527
- loadDataFromSource(savedData, false);
528
- console.log('Loaded saved form data');
529
- } else {
530
- // Load from file
531
- loadDataFromSource(fileData, true);
532
- console.log('Loaded file data');
533
- }
534
- {% else %}
535
- if (savedData && savedData.length > 0) {
536
- // Load saved data
537
- loadDataFromSource(savedData, false);
538
- console.log('Loaded saved form data');
539
- } else {
540
- // Add default empty rows
541
- for (let i = 0; i < 5; i++) {
542
- addRow(null, true);
543
- }
544
- }
545
- {% endif %}
546
- }
547
-
548
- // Handle page unload
549
- window.addEventListener('beforeunload', function() {
550
- saveFormData();
551
- });
552
-
553
- // Initialize table when page loads
554
- document.addEventListener("DOMContentLoaded", function() {
555
- loadConfigData();
556
- });
557
- </script>
558
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/main/__init__.py DELETED
File without changes
ivoryos/routes/main/main.py DELETED
@@ -1,42 +0,0 @@
1
- from flask import Blueprint, render_template, current_app
2
- from flask_login import login_required
3
- from ivoryos.version import __version__ as ivoryos_version
4
-
5
- main = Blueprint('main', __name__, template_folder='templates/main')
6
-
7
- @main.route("/")
8
- @login_required
9
- def index():
10
- """
11
- .. :quickref: Home page; ivoryos home page
12
-
13
- Home page for all available routes
14
-
15
- .. http:get:: /
16
-
17
- """
18
- off_line = current_app.config["OFF_LINE"]
19
- return render_template('home.html', off_line=off_line, version=ivoryos_version)
20
-
21
-
22
- @main.route("/help")
23
- def help_info():
24
- """
25
- .. :quickref: Help page; ivoryos info page
26
-
27
- static information page
28
-
29
- .. http:get:: /help
30
-
31
- """
32
- sample_deck = """
33
- from vapourtec.sf10 import SF10
34
-
35
- # connect SF10 pump
36
- sf10 = SF10(device_port="com7")
37
-
38
- # start ivoryOS
39
- from ivoryos.app import ivoryos
40
- ivoryos(__name__)
41
- """
42
- return render_template('help.html', sample_deck=sample_deck)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/main/templates/main/help.html DELETED
@@ -1,141 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Documentation {% endblock %}
3
-
4
- {% block body %}
5
- <h2>Documentations:</h2>
6
- <div class="accordion accordion-flush" id="helpPage" >
7
- <div class="accordion-item">
8
- <h2 class="accordion-header">
9
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#overall" aria-expanded="false" aria-controls="helpPageControl">
10
- General information - What is ivoryOS?
11
- </button>
12
- </h2>
13
- <div id="overall" class="accordion-collapse collapse" data-bs-parent="#helpPage">
14
- <div class="accordion-body">
15
- <p>
16
- This web app aims to ease up the control of any Python-based self-driving labs (SDLs) by extracting and displaying functions and parameters for initialized modules dynamically.
17
- The modules can be hardware API, high-level functions, or experiment workflow. This can potentially be used for general visual programming purposes.
18
- </p>
19
- {# <p>Controller: single instrument mode and multi-instrument interface.</p>#}
20
- {# <p>Workflow editor: automation workflow editor#}
21
- </div>
22
- </div>
23
- </div>
24
- {# <p>In the Controller tab, you can connect any instrument or instruments from a complete automation deck. The GUI will parse and show available functions and prompt user to the input argument values if need.</p>#}
25
- {# <p>In the Build Workflow tab, you can edit your own automation workflow from scratch or from the workflow library, where stores the ongoing projects and some basic example workflows.</p>#}
26
-
27
- <div class="accordion-item">
28
- <h2 class="accordion-header">
29
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#connect-device" aria-expanded="false" aria-controls="helpPageControl">
30
- General information - start ivoryOS from script
31
- </button>
32
- </h2>
33
- <div id="connect-device" class="accordion-collapse collapse" data-bs-parent="#helpPage">
34
- <div class="accordion-body">
35
- <pre ><code class="python" >{{ sample_deck }}</code></pre>
36
- </div>
37
- </div>
38
- </div>
39
- <div class="accordion-item">
40
- <h2 class="accordion-header">
41
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#library" aria-expanded="false" aria-controls="helpPageControl">
42
- Script Editor - Library
43
- </button>
44
- </h2>
45
- <div id="library" class="accordion-collapse collapse" data-bs-parent="#helpPage">
46
- <div class="accordion-body">
47
- <p>In "Library" Tab, the interactive database displays all workflow information</p>
48
- <p>On top of the library page, you can click filter to choose your desired deck out of other automation platforms. You can also search for experiment keyword on the right</p>
49
- <p>Note that you can edit/delete your own workflow anytime, but in case you would like to adapt other author's workflow, you need to save as your own in a different file name.</p>
50
- </div>
51
- </div>
52
- </div>
53
- <div class="accordion-item">
54
- <h2 class="accordion-header">
55
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#editor" aria-expanded="false" aria-controls="helpPageControl">
56
- Script Editor - How to use script editor?
57
- </button>
58
- </h2>
59
- <div id="editor" class="accordion-collapse collapse" data-bs-parent="#helpPage">
60
- <div class="accordion-body">
61
- <p>You can browse and load other's workflow in "Library" Tab or go to "Build Workflow" Tab to build from scratch.</p>
62
- <p>On the workflow canvas, there are three coding blocks: Prep, Experiment and Clean up. As the names indicated, the Prep and Clean up are steps taken prior and after to the main experiment, they cannot be repeated. On the other hand, the Experiment section can be repeated by a designated times or <a href="#config">a .csv configuration file</a></p>
63
- <p>On the left panel, you can choose the deck profile on the top, and browse deck action and builtin python functions. Click to input your argument and add to the canvas, then drag the action to change their order.</p>
64
- <img src="{{url_for('static', filename='gui_annotation/Slide1.PNG')}}">
65
- </div>
66
- </div>
67
- </div>
68
- <div class="accordion-item">
69
- <h2 class="accordion-header">
70
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#editor-advanced" aria-expanded="false" aria-controls="helpPageControl">
71
- Script Editor - How to save data and configure reaction?
72
- </button>
73
- </h2>
74
- <div id="editor-advanced" class="accordion-collapse collapse" data-bs-parent="#helpPage">
75
- <div class="accordion-body">
76
-
77
- <img src="{{url_for('static', filename='gui_annotation/Slide2.PNG')}}">
78
- </div>
79
- </div>
80
- </div>
81
- <div class="accordion-item">
82
- <h2 class="accordion-header">
83
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#config" aria-expanded="false" aria-controls="helpPageControl">
84
- Run my script - How to execute my workflow?
85
- </button>
86
- </h2>
87
- <div id="config" class="accordion-collapse collapse" data-bs-parent="#helpPage">
88
- <div class="accordion-body">
89
- <p>For workflow with no parameters, simply input repeat times to run.</p>
90
- <p>To configure your workflow, if there are less or equal than 5 parameters, one can use the web form or import a csv file.
91
- For workflow with more than 5 parameters, web form is not available.</p>
92
- <p>If there is any output, Bayesian Optimization will be available for adaptive experimentation. Note that the outputs need to be numeric values. </p>
93
- {# <img src="{{url_for('static', filename='gui_annotation/Slide3.PNG')}}">#}
94
- </div>
95
- </div>
96
- </div>
97
-
98
-
99
-
100
- <div class="accordion-item">
101
- <h2 class="accordion-header">
102
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#controller" aria-expanded="false" aria-controls="helpPageControl">
103
- Control - How to use device interface?
104
- </button>
105
- </h2>
106
- <div id="controller" class="accordion-collapse collapse" data-bs-parent="#helpPage">
107
- <div class="accordion-body">
108
- <p>The function cards are draggable for customized layout. The hidden icon is to remove functions from the interface. </p>
109
- {# <p>(1) Import a python script where devices are connected using Deck Deice tab (e.g. applied to a complete automation deck with a deck.py file).</p>#}
110
- {# <p>(2) Connect devices in the web app using New Deice tab. There are builtin instruments, but you can always import your own API.</p>#}
111
- </div>
112
- </div>
113
- </div>
114
- <div class="accordion-item">
115
- <h2 class="accordion-header">
116
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#deck" aria-expanded="false" aria-controls="helpPageControl">
117
- Control - Connect new devices
118
- </button>
119
- </h2>
120
- <div id="deck" class="accordion-collapse collapse" data-bs-parent="#helpPage">
121
- <div class="accordion-body">
122
- <p>When temporarily connecting instruments, New device -> New connection -> Import API. Use absolute path of your Python API file.
123
- Then enter required info (e.g. COM port) to connect a device.</p>
124
- </div>
125
- </div>
126
- </div>
127
- <div class="accordion-item">
128
- <h2 class="accordion-header">
129
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#install" aria-expanded="false" aria-controls="helpPageControl">
130
- Supports
131
- </button>
132
- </h2>
133
- <div id="install" class="accordion-collapse collapse" data-bs-parent="#helpPage">
134
- <div class="accordion-body">
135
- This is project is a work in progress. In case of any bugs or suggestions, reach out to Ivory: ivoryzhang@chem.ubc.ca or create an issue on GitLab:
136
- <a href="https://gitlab.com/heingroup/ivoryos">https://gitlab.com/heingroup/ivoryos</a>.
137
- </div>
138
- </div>
139
- </div>
140
- </div>
141
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/routes/main/templates/main/home.html DELETED
@@ -1,103 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Welcome{% endblock %}
3
-
4
- {% block body %}
5
- <div class="p-4">
6
- <h1 class="mb-4" style="font-size: 3rem; font-weight: bold; color: #343a40;">
7
- Welcome
8
- </h1>
9
- <p class="mb-5">Version: {{ version }}</p>
10
-
11
- {% if enable_design %}
12
- <!-- Workflow Design Section -->
13
- <h4 class="mb-3">Workflow Design</h4>
14
- <div class="row">
15
- <div class="col-lg-6 mb-3 d-flex align-items-stretch">
16
- <div class="card rounded shadow-sm flex-fill">
17
- <div class="card-body">
18
- <h5 class="card-title">
19
- <i class="bi bi-folder2-open me-2"></i>Browse designs
20
- </h5>
21
- <p class="card-text">View all saved workflows from the database.</p>
22
- <a href="{{ url_for('database.load_from_database') }}" class="stretched-link"></a>
23
- </div>
24
- </div>
25
- </div>
26
- <div class="col-lg-6 mb-3 d-flex align-items-stretch">
27
- <div class="card rounded shadow-sm flex-fill">
28
- <div class="card-body">
29
- <h5 class="card-title">
30
- <i class="bi bi-pencil-square me-2"></i>Edit designs
31
- </h5>
32
- <p class="card-text">Create or modify workflows using available functions.</p>
33
- <a href="{{ url_for('design.experiment_builder') }}" class="stretched-link"></a>
34
- </div>
35
- </div>
36
- </div>
37
- </div>
38
- {% endif %}
39
-
40
- <!-- Workflow Control and Monitor Section -->
41
- <h4 class="mt-5 mb-3">Workflow Control & Monitoring</h4>
42
- <div class="row">
43
- <!-- Always visible: Experiment data -->
44
- <div class="col-lg-6 mb-3 d-flex align-items-stretch">
45
- <div class="card rounded shadow-sm flex-fill">
46
- <div class="card-body">
47
- <h5 class="card-title">
48
- <i class="bi bi-graph-up-arrow me-2"></i>Experiment data
49
- </h5>
50
- <p class="card-text">Browse workflow logs and output data.</p>
51
- <a href="{{ url_for('database.list_workflows') }}" class="stretched-link"></a>
52
- </div>
53
- </div>
54
- </div>
55
-
56
- <!-- Conditionally visible: Run current workflow -->
57
- {% if not off_line %}
58
- <div class="col-lg-6 mb-3 d-flex align-items-stretch">
59
- <div class="card rounded shadow-sm flex-fill">
60
- <div class="card-body">
61
- <h5 class="card-title">
62
- <i class="bi bi-play-circle me-2"></i>Run current workflow
63
- </h5>
64
- <p class="card-text">Execute workflows with configurable parameters.</p>
65
- <a href="{{ url_for('design.experiment_run') }}" class="stretched-link"></a>
66
- </div>
67
- </div>
68
- </div>
69
- {% endif %}
70
- </div>
71
-
72
-
73
-
74
- {% if not off_line %}
75
- <!-- Direct Control Section -->
76
- <h4 class="mt-5 mb-3">Direct Control</h4>
77
- <div class="row">
78
- <div class="col-lg-6 mb-3 d-flex align-items-stretch">
79
- <div class="card rounded shadow-sm flex-fill">
80
- <div class="card-body">
81
- <h5 class="card-title">
82
- <i class="bi bi-toggle-on me-2"></i>Direct control
83
- </h5>
84
- <p class="card-text">Manually control individual components.</p>
85
- <a href="{{ url_for('control.deck_controllers') }}" class="stretched-link"></a>
86
- </div>
87
- </div>
88
- </div>
89
- <div class="col-lg-6 mb-3 d-flex align-items-stretch">
90
- <div class="card rounded shadow-sm flex-fill">
91
- <div class="card-body">
92
- <h5 class="card-title">
93
- <i class="bi bi-usb-plug me-2"></i>Connect a new device
94
- </h5>
95
- <p class="card-text">Add new hardware temporarily or for testing purposes.</p>
96
- <a href="{{ url_for('control.controllers_home') }}" class="stretched-link"></a>
97
- </div>
98
- </div>
99
- </div>
100
- </div>
101
- {% endif %}
102
- </div>
103
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/static/favicon.ico DELETED
Binary file (15.4 kB)
 
ivoryos/static/js/overlay.js DELETED
@@ -1,12 +0,0 @@
1
- function addOverlayToButtons(buttonIds) {
2
- buttonIds.forEach(function(buttonId) {
3
- document.getElementById(buttonId).addEventListener('submit', function() {
4
- // Display the overlay
5
- document.getElementById('overlay').style.display = 'block';
6
- document.getElementById('overlay-text').innerText = `Processing ${buttonId}...`;
7
- });
8
- });
9
- }
10
-
11
- // buttonIds should be set dynamically in your HTML template
12
- addOverlayToButtons(buttonIds);
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/static/js/socket_handler.js DELETED
@@ -1,125 +0,0 @@
1
- document.addEventListener("DOMContentLoaded", function() {
2
- var socket = io.connect('http://' + document.domain + ':' + location.port);
3
- socket.on('connect', function() {
4
- console.log('Connected');
5
- });
6
- socket.on('progress', function(data) {
7
- var progress = data.progress;
8
- console.log(progress);
9
- // Update the progress bar's width and appearance
10
- var progressBar = document.getElementById('progress-bar-inner');
11
- progressBar.style.width = progress + '%';
12
- progressBar.setAttribute('aria-valuenow', progress);
13
- const runPanel = document.getElementById("run-panel");
14
- const codePanel = document.getElementById("code-panel");
15
- if (progress === 1) {
16
- if (runPanel) runPanel.style.display = "none";
17
- if (codePanel) {
18
- codePanel.style.display = "block";
19
- codePanel.scrollIntoView({ behavior: "smooth" });
20
- }
21
- progressBar.classList.remove('bg-success');
22
- progressBar.classList.remove('bg-danger');
23
- progressBar.classList.add('progress-bar-animated');
24
- }
25
- if (progress === 100) {
26
- // Remove animation and set green color when 100% is reached
27
- progressBar.classList.remove('progress-bar-animated');
28
- progressBar.classList.add('bg-success'); // Bootstrap class for green color
29
- setTimeout(() => {
30
- if (runPanel) runPanel.style.display = "block";
31
- if (codePanel) codePanel.style.display = "none";
32
- }, 1000); // Small delay to let users see the completion
33
- }
34
- });
35
-
36
- socket.on('error', function(errorData) {
37
- console.error("Error received:", errorData);
38
- var progressBar = document.getElementById('progress-bar-inner');
39
-
40
- progressBar.classList.remove('bg-success');
41
- progressBar.classList.add('bg-danger'); // Red color for error
42
- // Show error modal
43
- var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
44
- document.getElementById('error-message').innerText = "An error occurred: " + errorData.message;
45
- errorModal.show();
46
-
47
- });
48
-
49
- // Handle Pause/Resume Button
50
- document.getElementById('pause-resume').addEventListener('click', function() {
51
- socket.emit('pause');
52
- console.log('Pause/Resume is toggled.');
53
- var button = this;
54
- var icon = button.querySelector("i");
55
-
56
- // Toggle Pause and Resume
57
- if (icon.classList.contains("bi-pause-circle")) {
58
- icon.classList.remove("bi-pause-circle");
59
- icon.classList.add("bi-play-circle");
60
- button.innerHTML = '<i class="bi bi-play-circle"></i>';
61
- button.setAttribute("title", "Resume execution");
62
- } else {
63
- icon.classList.remove("bi-play-circle");
64
- icon.classList.add("bi-pause-circle");
65
- button.innerHTML = '<i class="bi bi-pause-circle"></i>';
66
- button.setAttribute("title", "Pause execution");
67
- }
68
- });
69
-
70
- // Handle Modal Buttons
71
- document.getElementById('continue-btn').addEventListener('click', function() {
72
- socket.emit('pause'); // Resume execution
73
- console.log("Execution resumed.");
74
- });
75
-
76
- document.getElementById('retry-btn').addEventListener('click', function() {
77
- socket.emit('retry'); // Resume execution
78
- console.log("Execution resumed, retrying.");
79
- });
80
-
81
- document.getElementById('stop-btn').addEventListener('click', function() {
82
- socket.emit('pause'); // Resume execution
83
- socket.emit('abort_current'); // Stop execution
84
- console.log("Execution stopped.");
85
-
86
- // Reset UI back to initial state
87
- document.getElementById("code-panel").style.display = "none";
88
- document.getElementById("run-panel").style.display = "block";
89
- });
90
-
91
- socket.on('log', function(data) {
92
- var logMessage = data.message;
93
- console.log(logMessage);
94
- $('#logging-panel').append(logMessage + "<br>");
95
- $('#logging-panel').scrollTop($('#logging-panel')[0].scrollHeight);
96
- });
97
-
98
- document.getElementById('abort-pending').addEventListener('click', function() {
99
- var confirmation = confirm("Are you sure you want to stop after this iteration?");
100
- if (confirmation) {
101
- socket.emit('abort_pending');
102
- console.log('Abort action sent to server.');
103
- }
104
- });
105
- document.getElementById('abort-current').addEventListener('click', function() {
106
- var confirmation = confirm("Are you sure you want to stop after this step?");
107
- if (confirmation) {
108
- socket.emit('abort_current');
109
- console.log('Stop action sent to server.');
110
- }
111
- });
112
-
113
- socket.on('execution', function(data) {
114
- // Remove highlighting from all lines
115
- document.querySelectorAll('pre code').forEach(el => el.style.backgroundColor = '');
116
-
117
- // Get the currently executing line and highlight it
118
- let executingLine = document.getElementById(data.section);
119
- if (executingLine) {
120
- executingLine.style.backgroundColor = '#cce5ff'; // Highlight
121
- executingLine.style.transition = 'background-color 0.3s ease-in-out';
122
-
123
- }
124
- });
125
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/static/js/sortable_card.js DELETED
@@ -1,24 +0,0 @@
1
- $(function() {
2
- $("#sortable-grid").sortable({
3
- items: ".card",
4
- cursor: "move",
5
- opacity: 0.7,
6
- revert: true,
7
- placeholder: "ui-state-highlight",
8
- start: function(event, ui) {
9
- ui.placeholder.height(ui.item.height());
10
- },
11
- update: function() {
12
- const newOrder = $("#sortable-grid").sortable("toArray");
13
- $.ajax({
14
- url: saveOrderUrl, // saveOrderUrl should be set dynamically in your HTML template
15
- method: 'POST',
16
- contentType: 'application/json',
17
- data: JSON.stringify({ order: newOrder }),
18
- success: function() {
19
- console.log('Order saved');
20
- }
21
- });
22
- }
23
- }).disableSelection();
24
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/static/js/sortable_design.js DELETED
@@ -1,105 +0,0 @@
1
- $(document).ready(function () {
2
- let dropTargetId = ""; // Store the ID of the drop target
3
-
4
- $("#list ul").sortable({
5
- cancel: ".unsortable",
6
- opacity: 0.8,
7
- cursor: "move",
8
- placeholder: "drop-placeholder",
9
- update: function () {
10
- var item_order = [];
11
- $("ul.reorder li").each(function () {
12
- item_order.push($(this).attr("id"));
13
- });
14
- var order_string = "order=" + item_order.join(",");
15
-
16
- $.ajax({
17
- method: "POST",
18
- url: updateListUrl,
19
- data: order_string,
20
- cache: false,
21
- success: function (data) {
22
- $("#response").html(data);
23
- $("#response").slideDown("slow");
24
- window.location.href = window.location.href;
25
- }
26
- });
27
- }
28
- });
29
-
30
- // Make Entire Accordion Item Draggable
31
- $(".accordion-item").on("dragstart", function (event) {
32
- let formHtml = $(this).find(".accordion-body").html(); // Get the correct form
33
- event.originalEvent.dataTransfer.setData("form", formHtml || ""); // Store form HTML
34
- event.originalEvent.dataTransfer.setData("action", $(this).find(".draggable-action").data("action"));
35
- event.originalEvent.dataTransfer.setData("id", $(this).find(".draggable-action").attr("id"));
36
-
37
- $(this).addClass("dragging");
38
- });
39
-
40
-
41
- $("#list ul, .canvas").on("dragover", function (event) {
42
- event.preventDefault();
43
- let $target = $(event.target).closest("li");
44
-
45
- // If we're over a valid <li> element in the list
46
- if ($target.length) {
47
- dropTargetId = $target.attr("id") || ""; // Store the drop target ID
48
-
49
- $(".drop-placeholder").remove(); // Remove existing placeholders
50
- $("<li class='drop-placeholder'></li>").insertBefore($target); // Insert before the target element
51
- } else if (!$("#list ul").children().length && $(this).hasClass("canvas")) {
52
- $(".drop-placeholder").remove(); // Remove any placeholder
53
- // $("#list ul").append("<li class='drop-placeholder'></li>"); // Append placeholder to canvas
54
- } else {
55
- dropTargetId = ""; // Append placeholder to canvas
56
- }
57
- });
58
-
59
- $("#list ul, .canvas").on("dragleave", function () {
60
- $(".drop-placeholder").remove(); // Remove placeholder on leave
61
- });
62
-
63
- $("#list ul, .canvas").on("drop", function (event) {
64
- event.preventDefault();
65
-
66
- var actionName = event.originalEvent.dataTransfer.getData("action");
67
- var actionId = event.originalEvent.dataTransfer.getData("id");
68
- var formHtml = event.originalEvent.dataTransfer.getData("form"); // Retrieve form HTML
69
- let listLength = $("ul.reorder li").length;
70
- dropTargetId = dropTargetId || listLength + 1; // Assign a "last" ID or unique identifier
71
- $(".drop-placeholder").remove();
72
- // Trigger the modal with the appropriate action
73
- triggerModal(formHtml, actionName, actionId, dropTargetId);
74
-
75
- });
76
-
77
- // Function to trigger the modal (same for both buttons and accordion items)
78
- function triggerModal(formHtml, actionName, actionId, dropTargetId) {
79
- if (formHtml && formHtml.trim() !== "") {
80
- var $form = $("<div>").html(formHtml); // Convert HTML string to jQuery object
81
-
82
- // Create a hidden input for the drop target ID
83
- var $hiddenInput = $("<input>")
84
- .attr("type", "hidden")
85
- .attr("name", "drop_target_id")
86
- .attr("id", "dropTargetInput")
87
- .val(dropTargetId);
88
-
89
- // Insert before the submit button
90
- $form.find("button[type='submit']").before($hiddenInput);
91
-
92
- $("#modalFormFields").empty().append($form.children());
93
- $("#dropModal").modal("show"); // Show modal
94
-
95
- // Store and display drop target ID in the modal
96
- $("#modalDropTarget").text(dropTargetId || "N/A");
97
-
98
- $("#modalFormFields").data("action-id", actionId);
99
- $("#modalFormFields").data("action-name", actionName);
100
- $("#modalFormFields").data("drop-target-id", dropTargetId);
101
- } else {
102
- console.error("Form HTML is undefined or empty!");
103
- }
104
- }
105
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/static/logo.webp DELETED
Binary file (15 kB)
 
ivoryos/static/style.css DELETED
@@ -1,211 +0,0 @@
1
- .login {
2
- width: 500px;
3
- padding: 8% 0 0;
4
- margin: auto;
5
- align-content: center;
6
- }
7
- .card{
8
- display: flex;
9
- justify-content: flex-end;
10
- cursor: move;
11
- border: none;
12
- transition: transform 0.3s ease, box-shadow 0.3s ease;
13
-
14
- }
15
-
16
- .card a {
17
- text-decoration: none;
18
- }
19
- .card-title {
20
- color: #007bff; /* Bootstrap primary color */
21
- }
22
- .grid-container {
23
- display: grid;
24
- grid-template-columns: repeat(auto-fit, minmax(350px, 400px));
25
- gap: 25px;
26
- padding: 10px;
27
- }
28
- .navbar-nav {
29
- font-size: larger;
30
- }
31
- .navbar-nav li{
32
- margin-right:10px;
33
- margin-left:10px;
34
- }
35
-
36
- .canvas {
37
- height: 70vh;
38
- overflow: hidden;
39
- overflow-y: scroll;
40
- background: #e8e8cd;
41
- background-size: 20px 20px;
42
- background-image: radial-gradient(circle, cadetblue 1px, rgba(0, 0, 0, 0) 1px);
43
- }
44
-
45
- .canvas content{
46
- margin-top: 10px;
47
- }
48
- body {
49
- padding-top: 100px;
50
- background-color: #f8f9fa; /* Light grey background */
51
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
52
- }
53
- /*.run-panel-row-height {*/
54
- /* height: 80vh;*/
55
- /*}*/
56
- pre {
57
- tab-size: 4;
58
- }
59
- .scroll-column{
60
- height: 90vh;
61
- overflow: hidden;
62
- overflow-y: scroll;
63
- }
64
-
65
- label {
66
- display:block
67
- }
68
-
69
- .scroll {
70
- overflow: auto;
71
- -webkit-overflow-scrolling: touch; /* enables momentum-scrolling on iOS */
72
- position: absolute;
73
- top: 0;
74
- left: 0;
75
- right: 0;
76
- bottom: 0;
77
- }
78
-
79
-
80
- /*Remove the scrollbar from Chrome, Safari, Edge and IE*/
81
- ::-webkit-scrollbar {
82
- background: transparent;
83
- }
84
-
85
- * {
86
- -ms-overflow-style: none !important;
87
- }
88
- .script-table{
89
- background: white;
90
- width: fit-content;
91
- }
92
- .script-table td {
93
- border-bottom: none;
94
- border-top: cadetblue;
95
- }
96
- .script-table th{
97
- border-top: cadetblue;
98
- border-bottom: none;
99
- }
100
-
101
- .bottom-button {
102
- position: absolute;
103
- bottom: 20px;
104
- }
105
-
106
- hr.vertical {
107
- width: 5px;
108
- height: 100%;
109
- display: inline-block;
110
- /* or height in PX */
111
- }
112
- .list-group a {
113
- text-align: left;
114
- }
115
-
116
- .tray input[type="checkbox"]:checked + label{
117
- background: radial-gradient(circle, white 40%, midnightblue, dodgerblue 43%);
118
- }
119
-
120
- .btn-vial{
121
- height: 50px;
122
- width: 50px;
123
- border-radius: 50%;
124
- background: lightgray;
125
- margin-right: 10px;
126
- margin-top: 10px;
127
- /*text-align: center;*/
128
-
129
- }
130
-
131
- .tray {
132
- width: 70px;
133
- height: 70px;
134
- display: inline-block;
135
- background: darkgrey;
136
- /*text-align:center;*/
137
- vertical-align: middle;
138
- }
139
-
140
- .disabled-link {
141
- pointer-events: none;
142
- color: currentColor;
143
- cursor: not-allowed;
144
- opacity: 0.5;
145
- text-decoration: none;
146
- }
147
-
148
-
149
- .controller-card a {
150
- text-decoration: none;
151
- }
152
-
153
- #logging-panel {
154
- flex-grow: 1;
155
- height: 50vh;
156
- overflow-y: auto;
157
- background-color: #f5f5f5;
158
- padding: 10px;
159
- }
160
-
161
- .dropdown:hover .dropdown-menu {
162
- display: block;
163
- }
164
-
165
- #reorder {
166
- overflow-y: scroll;
167
- -webkit-overflow-scrolling: touch;
168
- }
169
-
170
- .accordion-item.design-control .accordion-button.collapsed {
171
- background-color:#c1f2f1 !important;
172
- }
173
- .accordion-item.design-control .accordion-button {
174
- background-color:#b3dad9 !important;
175
- }
176
- .accordion-item.design-control .accordion-button:hover {
177
- background-color:#b3dad9 !important;
178
- }
179
-
180
- .accordion-item.text-to-code .accordion-button.collapsed {
181
- background-color: #cdc1f2 !important;
182
- }
183
- .accordion-item.text-to-code .accordion-button {
184
- background-color: #cdc1f2 !important;
185
- }
186
- .accordion-item.text-to-code .accordion-button:hover {
187
- background-color: #b8afdc !important;
188
- }
189
- .overlay {
190
- position: fixed;
191
- top: 0;
192
- left: 0;
193
- width: 100%;
194
- height: 100%;
195
- background-color: rgba(0, 0, 0, 0.5);
196
- display: none;
197
- z-index: 1000;
198
- text-align: center;
199
- color: white;
200
- font-size: 24px;
201
- padding-top: 20%;
202
- }
203
- .drop-placeholder {
204
- height: 2px !important; /* Keep it very thin */
205
- min-height: 2px !important;
206
- margin: 0 !important;
207
- padding: 0 !important;
208
- background: rgba(0, 0, 0, 0.2); /* Slight visibility */
209
- border-radius: 2px;
210
- list-style: none; /* Remove any default list styling */
211
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/templates/base.html DELETED
@@ -1,157 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>{% block title %}{% endblock %}</title>
7
- {#bootstrap#}
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css" integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">
9
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
11
- {#static#}
12
- <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
13
- <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
14
- {#for python code displaying#}
15
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
16
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js" integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N" crossorigin="anonymous"></script>
17
- <script>hljs.highlightAll();</script>
18
- {#drag design#}
19
- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
20
- {#drag card#}
21
- <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
22
- </head>
23
- <body>
24
- <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
25
- <div class= "container">
26
- {# {{ module_config }}#}
27
- <a class="navbar-brand" href="{{ url_for('main.index') }}">
28
- <img src="{{url_for('static', filename='logo.webp')}}" alt="Logo" height="60" class="d-inline-block align-text-bottom">
29
- </a>
30
- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
31
- <span class="navbar-toggler-icon"></span>
32
- </button>
33
-
34
- <div class="collapse navbar-collapse" id="navbarSupportedContent">
35
- <ul class="navbar-nav mr-auto">
36
- <li class="nav-item">
37
- <a class="nav-link" href="{{ url_for('main.index') }}" aria-current="page">Home</a>
38
- </li>
39
- {% if enable_design %}
40
- <li class="nav-item">
41
- <a class="nav-link" href="{{ url_for('database.load_from_database') }}" aria-current="page">Library</a>
42
- </li>
43
- <li class="nav-item">
44
- <a class="nav-link" href="{{ url_for('design.experiment_builder') }}">Design</a>
45
- </li>
46
- <li class="nav-item">
47
- <a class="nav-link" href="{{ url_for('design.experiment_run') }}">Compile/Run</a>
48
- </li>
49
- <li class="nav-item">
50
- <a class="nav-link" href="{{ url_for('database.list_workflows') }}">Data</a>
51
- </li>
52
- {% endif %}
53
-
54
- <li class="nav-item">
55
- <a class="nav-link" href="{{ url_for('control.deck_controllers') }}">Devices</a></li>
56
- </li>
57
- {# <li class="nav-item">#}
58
- {# <a class="nav-link" href="{{ url_for('control.controllers_home') }}">Temp Devices</a></li>#}
59
- {# </li>#}
60
- {# <li class="nav-item">#}
61
- {# <a class="nav-link" href="{{ url_for('main.help_info') }}">About</a>#}
62
- {# </li>#}
63
- {% if plugins %}
64
- {% for plugin in plugins %}
65
- <li class="nav-item">
66
- <a class="nav-link" href="{{ url_for(plugin+'.main') }}">{{ plugin.capitalize() }}</a></li>
67
- </li>
68
- {% endfor %}
69
- {% endif %}
70
- </ul>
71
- <ul class="navbar-nav ms-auto">
72
-
73
- {% if session["user"] %}
74
- <div class="dropdown">
75
- <li class="nav-item " aria-expanded="false"><i class="bi bi-person-circle"></i> {{ session["user"] }}</li>
76
- <ul class="dropdown-menu">
77
- <li><a class="dropdown-item" href="{{ url_for("auth.logout") }}" role="button" aria-expanded="false">Logout</a></li>
78
- </ul>
79
-
80
- </div>
81
- {% else %}
82
- <li class="nav-item">
83
- <a class="nav-link" href="{{ url_for("auth.login") }}">Login</a>
84
- </li>
85
- {% endif %}
86
- {# <li class="nav-item">#}
87
- {# <a class="nav-link"href="{{ url_for("signup") }}">Signup</a>#}
88
- {# </li>#}
89
- </ul>
90
- </div>
91
- </div>
92
- </nav>
93
-
94
- <div class= "container">
95
- <div class="flash">
96
- {% with messages = get_flashed_messages() %}
97
- {% if messages %}
98
- <div class="alert alert-warning">
99
- Message:
100
- {% for message in messages %}
101
- <div >
102
- {{ message|safe }}
103
- </div>
104
- {% endfor %}
105
- </div>
106
- {% endif %}
107
- {% endwith %}
108
- </div>
109
- {% block body %}{% endblock %}
110
- </div>
111
-
112
- <div class="modal fade" id="importModal" tabindex="-1" aria-labelledby="importModal" aria-hidden="true" >
113
- <div class="modal-dialog">
114
- <div class="modal-content">
115
- <div class="modal-header">
116
- <h1 class="modal-title fs-5" id="importModal">Import deck by file path</h1>
117
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
118
- </div>
119
- <form method="POST" action="{{ url_for('control.import_deck') }}" enctype="multipart/form-data">
120
- <div class="modal-body">
121
- <h5>from connection history</h5>
122
- <div class="form-group">
123
- <select class="form-select" name="filepath">
124
- <option disabled selected value> -- select an option -- </option>
125
- {% for connection in history %}
126
- <option style="overflow-wrap: break-word;" name="filepath" id="filepath" value="{{connection}}">{{connection}}</option>
127
- {% endfor %}
128
- {# <option>clear history</option>#}
129
- </select>
130
- </div>
131
- <h5>input manually</h5>
132
- <div class="input-group mb-3">
133
- <label class="input-group-text" for="filepath">File Path:</label>
134
- <input type="text" class="form-control" name="filepath" id="filepath">
135
- </div>
136
- <div class="input-group mb-3">
137
- <div class="form-check">
138
- <input type="checkbox" class="form-check-input" id="update" name="update" value="update">
139
- <label class="form-check-label" for="update">Update editor config</label>
140
- </div>
141
- </div>
142
-
143
- <div class="modal-footer">
144
- <div class="form-check">
145
- <input type="checkbox" class="form-check-input" id="dismiss" name="dismiss" value="dismiss">
146
- <label class="form-check-label" for="dismiss">Don't remind me</label>
147
- </div>
148
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
149
- <button type="submit" class="btn btn-primary"> Save </button>
150
- </div>
151
- </div>
152
- </form>
153
- </div>
154
- </div>
155
- </div>
156
- </body>
157
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/__init__.py DELETED
File without changes
ivoryos/utils/bo_campaign.py DELETED
@@ -1,87 +0,0 @@
1
- from ivoryos.utils.utils import install_and_import
2
-
3
-
4
- def ax_init_form(data, arg_types):
5
- """
6
- create Ax campaign from the web form input
7
- :param data:
8
- """
9
- install_and_import("ax", "ax-platform")
10
- parameter, objectives = ax_wrapper(data, arg_types)
11
- from ax.service.ax_client import AxClient
12
- ax_client = AxClient()
13
- ax_client.create_experiment(parameter, objectives=objectives)
14
- return ax_client
15
-
16
-
17
- def ax_wrapper(data: dict, arg_types: list):
18
- """
19
- Ax platform wrapper function for creating optimization campaign parameters and objective from the web form input
20
- :param data: e.g.,
21
- {
22
- "param_1_type": "range", "param_1_value": [1,2],
23
- "param_2_type": "range", "param_2_value": [1,2],
24
- "obj_1_min": True,
25
- "obj_2_min": True
26
- }
27
- :return: the optimization campaign parameters
28
- parameter=[
29
- {"name": "param_1", "type": "range", "bounds": [1,2]},
30
- {"name": "param_1", "type": "range", "bounds": [1,2]}
31
- ]
32
- objectives=[
33
- {"name": "obj_1", "min": True, "threshold": None},
34
- {"name": "obj_2", "min": True, "threshold": None},
35
- ]
36
- """
37
- from ax.service.utils.instantiation import ObjectiveProperties
38
- parameter = []
39
- objectives = {}
40
- # Iterate through the webui_data dictionary
41
- for key, value in data.items():
42
- # Check if the key corresponds to a parameter type
43
- if "_type" in key:
44
- param_name = key.split("_type")[0]
45
- param_type = value
46
- param_value = data[f"{param_name}_value"].split(",")
47
- try:
48
- values = [float(v) for v in param_value]
49
- except Exception:
50
- values = param_value
51
- if param_type == "range":
52
- param = {"name": param_name, "type": param_type, "bounds": values}
53
- if param_type == "choice":
54
- param = {"name": param_name, "type": param_type, "values": values}
55
- if param_type == "fixed":
56
- param = {"name": param_name, "type": param_type, "value": values[0]}
57
- _type = arg_types[param_name] if arg_types[param_name] in ["str", "bool", "int"] else "float"
58
- param.update({"value_type": _type})
59
- parameter.append(param)
60
- elif key.endswith("_min"):
61
- if not value == 'none':
62
- obj_name = key.split("_min")[0]
63
- is_min = True if value == "minimize" else False
64
-
65
- threshold = None if f"{obj_name}_threshold" not in data else data[f"{obj_name}_threshold"]
66
- properties = ObjectiveProperties(minimize=is_min)
67
- objectives[obj_name] = properties
68
-
69
- return parameter, objectives
70
-
71
-
72
- def ax_init_opc(bo_args):
73
- install_and_import("ax", "ax-platform")
74
- from ax.service.ax_client import AxClient
75
- from ax.service.utils.instantiation import ObjectiveProperties
76
-
77
- ax_client = AxClient()
78
- objectives = bo_args.get("objectives")
79
- objectives_formatted = {}
80
- for obj in objectives:
81
- obj_name = obj.get("name")
82
- minimize = obj.get("minimize")
83
- objectives_formatted[obj_name] = ObjectiveProperties(minimize=minimize)
84
- bo_args["objectives"] = objectives_formatted
85
- ax_client.create_experiment(**bo_args)
86
-
87
- return ax_client
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/client_proxy.py DELETED
@@ -1,57 +0,0 @@
1
- # import argparse
2
- import os
3
-
4
- # import requests
5
-
6
- # session = requests.Session()
7
-
8
-
9
- # Function to create class and methods dynamically
10
- def create_function(url, class_name, functions):
11
- class_template = f'class {class_name.capitalize()}:\n url = "{url}ivoryos/api/control/deck.{class_name}"\n'
12
-
13
- for function_name, details in functions.items():
14
- signature = details['signature']
15
- docstring = details.get('docstring', '')
16
-
17
- # Creating the function definition
18
- method = f' def {function_name}{signature}:\n'
19
- if docstring:
20
- method += f' """{docstring}"""\n'
21
-
22
- # Generating the session.post code for sending data
23
- method += ' return session.post(self.url, data={'
24
- method += f'"hidden_name": "{function_name}"'
25
-
26
- # Extracting the parameters from the signature string for the data payload
27
- param_str = signature[6:-1] # Remove the "(self" and final ")"
28
- params = [param.strip() for param in param_str.split(',')] if param_str else []
29
-
30
- for param in params:
31
- param_name = param.split(':')[0].strip() # Split on ':' and get parameter name
32
- method += f', "{param_name}": {param_name}'
33
-
34
- method += '}).json()\n'
35
- class_template += method + '\n'
36
-
37
- return class_template
38
-
39
- # Function to export the generated classes to a Python script
40
- def export_to_python(class_definitions, path):
41
- with open(os.path.join(path, "generated_proxy.py"), 'w') as f:
42
- # Writing the imports at the top of the script
43
- f.write('import requests\n\n')
44
- f.write('session = requests.Session()\n\n')
45
-
46
- # Writing each class definition to the file
47
- for class_name, class_def in class_definitions.items():
48
- f.write(class_def)
49
- f.write('\n')
50
-
51
- # Creating instances of the dynamically generated classes
52
- for class_name in class_definitions.keys():
53
- instance_name = class_name.lower() # Using lowercase for instance names
54
- f.write(f'{instance_name} = {class_name.capitalize()}()\n')
55
-
56
-
57
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/db_models.py DELETED
@@ -1,700 +0,0 @@
1
- import ast
2
- import builtins
3
- import json
4
- import keyword
5
- import re
6
- import uuid
7
- from datetime import datetime
8
- from typing import Dict
9
-
10
- from flask_login import UserMixin
11
- from flask_sqlalchemy import SQLAlchemy
12
- from sqlalchemy_utils import JSONType
13
-
14
- db = SQLAlchemy()
15
-
16
-
17
- class User(db.Model, UserMixin):
18
- __tablename__ = 'user'
19
- # id = db.Column(db.Integer)
20
- username = db.Column(db.String(50), primary_key=True, unique=True, nullable=False)
21
- # email = db.Column(db.String)
22
- hashPassword = db.Column(db.String(255))
23
-
24
- # password = db.Column()
25
- def __init__(self, username, password):
26
- # self.id = id
27
- self.username = username
28
- # self.email = email
29
- self.hashPassword = password
30
-
31
- def get_id(self):
32
- return self.username
33
-
34
-
35
- class Script(db.Model):
36
- __tablename__ = 'script'
37
- # id = db.Column(db.Integer, primary_key=True)
38
- name = db.Column(db.String(50), primary_key=True, unique=True)
39
- deck = db.Column(db.String(50), nullable=True)
40
- status = db.Column(db.String(50), nullable=True)
41
- script_dict = db.Column(JSONType, nullable=True)
42
- time_created = db.Column(db.String(50), nullable=True)
43
- last_modified = db.Column(db.String(50), nullable=True)
44
- id_order = db.Column(JSONType, nullable=True)
45
- editing_type = db.Column(db.String(50), nullable=True)
46
- author = db.Column(db.String(50), nullable=False)
47
- # registered = db.Column(db.Boolean, nullable=True, default=False)
48
-
49
- def __init__(self, name=None, deck=None, status=None, script_dict: dict = None, id_order: dict = None,
50
- time_created=None, last_modified=None, editing_type=None, author: str = None,
51
- # registered:bool=False,
52
- python_script: str = None
53
- ):
54
- if script_dict is None:
55
- script_dict = {"prep": [], "script": [], "cleanup": []}
56
- elif type(script_dict) is not dict:
57
- script_dict = json.loads(script_dict)
58
- if id_order is None:
59
- id_order = {"prep": [], "script": [], "cleanup": []}
60
- elif type(id_order) is not dict:
61
- id_order = json.loads(id_order)
62
- if status is None:
63
- status = 'editing'
64
- if time_created is None:
65
- time_created = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
66
- if last_modified is None:
67
- last_modified = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
68
- if editing_type is None:
69
- editing_type = "script"
70
-
71
- self.name = name
72
- self.deck = deck
73
- self.status = status
74
- self.script_dict = script_dict
75
- self.time_created = time_created
76
- self.last_modified = last_modified
77
- self.id_order = id_order
78
- self.editing_type = editing_type
79
- self.author = author
80
- self.python_script = python_script
81
- # self.r = registered
82
-
83
- def as_dict(self):
84
- dict = self.__dict__
85
- dict.pop('_sa_instance_state', None)
86
- return dict
87
-
88
- def get(self):
89
- workflows = db.session.query(Script).all()
90
- # result = script_schema.dump(workflows)
91
- return workflows
92
-
93
- def find_by_uuid(self, uuid):
94
- for stype in self.script_dict:
95
- for action in self.script_dict[stype]:
96
-
97
- if action['uuid'] == int(uuid):
98
- return action
99
-
100
- def _convert_type(self, args, arg_types):
101
- if arg_types in ["list", "tuple", "set"]:
102
- try:
103
- args = ast.literal_eval(args)
104
- return args
105
- except Exception:
106
- pass
107
- if type(arg_types) is not list:
108
- arg_types = [arg_types]
109
- for arg_type in arg_types:
110
- try:
111
- # print(arg_type)
112
- args = eval(f"{arg_type}('{args}')")
113
- return
114
- except Exception:
115
-
116
- pass
117
- raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
118
-
119
- def update_by_uuid(self, uuid, args, output):
120
- action = self.find_by_uuid(uuid)
121
- if not action:
122
- return
123
- arg_types = action['arg_types']
124
- if type(action['args']) is dict:
125
- # pass
126
- self.eval_list(args, arg_types)
127
- else:
128
- pass
129
- action['args'] = args
130
- action['return'] = output
131
-
132
- @staticmethod
133
- def eval_list(args, arg_types):
134
- for arg in args:
135
- arg_type = arg_types[arg]
136
- if arg_type in ["list", "tuple", "set"]:
137
-
138
- if type(arg) is str and not args[arg].startswith("#"):
139
- # arg_types = arg_types[arg]
140
- # if arg_types in ["list", "tuple", "set"]:
141
- convert_type = getattr(builtins, arg_type) # Handle unknown types s
142
- try:
143
- output = ast.literal_eval(args[arg])
144
- if type(output) not in [list, tuple, set]:
145
- output = [output]
146
- args[arg] = convert_type(output)
147
- # return args
148
- except ValueError:
149
- _list = ''.join(args[arg]).split(',')
150
- # convert_type = getattr(builtins, arg_types) # Handle unknown types s
151
- args[arg] = convert_type([s.strip() for s in _list])
152
-
153
- @property
154
- def stypes(self):
155
- return list(self.script_dict.keys())
156
-
157
- @property
158
- def currently_editing_script(self):
159
- return self.script_dict[self.editing_type]
160
-
161
- @currently_editing_script.setter
162
- def currently_editing_script(self, script):
163
- self.script_dict[self.editing_type] = script
164
-
165
- @property
166
- def currently_editing_order(self):
167
- return self.id_order[self.editing_type]
168
-
169
- @currently_editing_order.setter
170
- def currently_editing_order(self, script):
171
- self.id_order[self.editing_type] = script
172
-
173
- def update_time_stamp(self):
174
- self.last_modified = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
175
-
176
- def get_script(self, stype: str):
177
- return self.script_dict[stype]
178
-
179
- def isEmpty(self) -> bool:
180
- if not (self.script_dict['script'] or self.script_dict['prep'] or self.script_dict['cleanup']):
181
- return True
182
- return False
183
-
184
- def _sort(self, script_type):
185
- if len(self.id_order[script_type]) > 0:
186
- for action in self.script_dict[script_type]:
187
- for i in range(len(self.id_order[script_type])):
188
- if action['id'] == int(self.id_order[script_type][i]):
189
- # print(i+1)
190
- action['id'] = i + 1
191
- break
192
- self.id_order[script_type].sort()
193
- if not int(self.id_order[script_type][-1]) == len(self.script_dict[script_type]):
194
- new_order = list(range(1, len(self.script_dict[script_type]) + 1))
195
- self.id_order[script_type] = [str(i) for i in new_order]
196
- self.script_dict[script_type].sort(key=lambda x: x['id'])
197
-
198
- def sort_actions(self, script_type=None):
199
- if script_type:
200
- self._sort(script_type)
201
- else:
202
- for i in self.stypes:
203
- self._sort(i)
204
-
205
- def add_action(self, action: dict, insert_position=None):
206
- current_len = len(self.currently_editing_script)
207
- action_to_add = action.copy()
208
- action_to_add['id'] = current_len + 1
209
- action_to_add['uuid'] = uuid.uuid4().fields[-1]
210
- self.currently_editing_script.append(action_to_add)
211
- self._insert_action(insert_position, current_len)
212
- self.update_time_stamp()
213
-
214
- def add_variable(self, statement, variable, type, insert_position=None):
215
- variable = self.validate_function_name(variable)
216
- convert_type = getattr(builtins, type)
217
- statement = convert_type(statement)
218
- current_len = len(self.currently_editing_script)
219
- uid = uuid.uuid4().fields[-1]
220
- action = {"id": current_len + 1, "instrument": 'variable', "action": variable,
221
- "args": {"statement": 'None' if statement == '' else statement}, "return": '', "uuid": uid,
222
- "arg_types": {"statement": type}}
223
- self.currently_editing_script.append(action)
224
- self._insert_action(insert_position, current_len)
225
- self.update_time_stamp()
226
-
227
- def _insert_action(self, insert_position, current_len, action_len:int=1):
228
-
229
- if insert_position is None:
230
- self.currently_editing_order.extend([str(current_len + i + 1) for i in range(action_len)])
231
- else:
232
- index = int(insert_position) - 1
233
- self.currently_editing_order[index:index] = [str(current_len + i + 1) for i in range(action_len)]
234
- self.sort_actions()
235
-
236
- def get_added_variables(self):
237
- added_variables: Dict[str, str] = {action["action"]: action["arg_types"]["statement"] for action in
238
- self.currently_editing_script if action["instrument"] == "variable"}
239
-
240
- return added_variables
241
-
242
- def get_output_variables(self):
243
- output_variables: Dict[str, str] = {action["return"]: "function_output" for action in
244
- self.currently_editing_script if action["return"]}
245
-
246
- return output_variables
247
-
248
- def get_variables(self):
249
- output_variables: Dict[str, str] = self.get_output_variables()
250
- added_variables = self.get_added_variables()
251
- output_variables.update(added_variables)
252
-
253
- return output_variables
254
-
255
- def validate_variables(self, kwargs):
256
- """
257
- Validates the kwargs passed to the Script
258
- """
259
- output_variables: Dict[str, str] = self.get_variables()
260
- # print(output_variables)
261
- for key, value in kwargs.items():
262
- if type(value) is str and value in output_variables:
263
- var_type = output_variables[value]
264
- kwargs[key] = {value: var_type}
265
- if isinstance(value, str) and value.startswith("#"):
266
- kwargs[key] = f"#{self.validate_function_name(value[1:])}"
267
- return kwargs
268
-
269
- def add_logic_action(self, logic_type: str, statement, insert_position=None):
270
- current_len = len(self.currently_editing_script)
271
- uid = uuid.uuid4().fields[-1]
272
- logic_dict = {
273
- "if":
274
- [
275
- {"id": current_len + 1, "instrument": 'if', "action": 'if',
276
- "args": {"statement": 'True' if statement == '' else statement},
277
- "return": '', "uuid": uid, "arg_types": {"statement": ''}},
278
- {"id": current_len + 2, "instrument": 'if', "action": 'else', "args": {}, "return": '',
279
- "uuid": uid},
280
- {"id": current_len + 3, "instrument": 'if', "action": 'endif', "args": {}, "return": '',
281
- "uuid": uid},
282
- ],
283
- "while":
284
- [
285
- {"id": current_len + 1, "instrument": 'while', "action": 'while',
286
- "args": {"statement": 'False' if statement == '' else statement}, "return": '', "uuid": uid,
287
- "arg_types": {"statement": ''}},
288
- {"id": current_len + 2, "instrument": 'while', "action": 'endwhile', "args": {}, "return": '',
289
- "uuid": uid},
290
- ],
291
-
292
- "wait":
293
- [
294
- {"id": current_len + 1, "instrument": 'wait', "action": "wait",
295
- "args": {"statement": 1 if statement == '' else statement},
296
- "return": '', "uuid": uid, "arg_types": {"statement": "float"}},
297
- ],
298
- "repeat":
299
- [
300
- {"id": current_len + 1, "instrument": 'repeat', "action": "repeat",
301
- "args": {"statement": 1 if statement == '' else statement}, "return": '', "uuid": uid,
302
- "arg_types": {"statement": "int"}},
303
- {"id": current_len + 2, "instrument": 'repeat', "action": 'endrepeat',
304
- "args": {}, "return": '', "uuid": uid},
305
- ],
306
- }
307
- action_list = logic_dict[logic_type]
308
- self.currently_editing_script.extend(action_list)
309
- self._insert_action(insert_position, current_len, len(action_list))
310
- self.update_time_stamp()
311
-
312
- def delete_action(self, id: int):
313
- """
314
- Delete the action by id (step number)
315
- """
316
- uid = next((action['uuid'] for action in self.currently_editing_script if action['id'] == int(id)), None)
317
- id_to_be_removed = [action['id'] for action in self.currently_editing_script if action['uuid'] == uid]
318
- order = self.currently_editing_order
319
- script = self.currently_editing_script
320
- self.currently_editing_order = [i for i in order if int(i) not in id_to_be_removed]
321
- self.currently_editing_script = [action for action in script if action['id'] not in id_to_be_removed]
322
- self.sort_actions()
323
- self.update_time_stamp()
324
-
325
- def duplicate_action(self, id: int):
326
- """
327
- duplicate action by id (step number), available only for non logic actions
328
- """
329
- action_to_duplicate = next((action for action in self.currently_editing_script if action['id'] == int(id)),
330
- None)
331
- insert_id = action_to_duplicate.get("id")
332
- self.add_action(action_to_duplicate)
333
- # print(self.currently_editing_script)
334
- if action_to_duplicate is not None:
335
- # Update IDs for all subsequent actions
336
- for action in self.currently_editing_script:
337
- if action['id'] > insert_id:
338
- action['id'] += 1
339
- self.currently_editing_script[-1]['id'] = insert_id + 1
340
- # Sort actions if necessary and update the time stamp
341
- self.sort_actions()
342
- self.update_time_stamp()
343
- else:
344
- raise ValueError("Action not found: Unable to duplicate the action with ID", id)
345
-
346
- def config(self, stype):
347
- """
348
- take the global script_dict
349
- :return: list of variable that require input
350
- """
351
- configure = []
352
- config_type_dict = {}
353
- for action in self.script_dict[stype]:
354
- args = action['args']
355
- if args is not None:
356
- if type(args) is not dict:
357
- if type(args) is str and args.startswith("#") and not args[1:] in configure:
358
- configure.append(args[1:])
359
- config_type_dict[args[1:]] = action['arg_types']
360
-
361
- else:
362
- for arg in args:
363
- if type(args[arg]) is str \
364
- and args[arg].startswith("#") \
365
- and not args[arg][1:] in configure:
366
- configure.append(args[arg][1:])
367
- if arg in action['arg_types']:
368
- if action['arg_types'][arg] == '':
369
- config_type_dict[args[arg][1:]] = "any"
370
- else:
371
- config_type_dict[args[arg][1:]] = action['arg_types'][arg]
372
- else:
373
- config_type_dict[args[arg][1:]] = "any"
374
- # todo
375
- return configure, config_type_dict
376
-
377
- def config_return(self):
378
- """
379
- take the global script_dict
380
- :return: list of variable that require input
381
- """
382
-
383
- return_list = set([action['return'] for action in self.script_dict['script'] if not action['return'] == ''])
384
- output_str = "return {"
385
- for i in return_list:
386
- output_str += "'" + i + "':" + i + ","
387
- output_str += "}"
388
- return output_str, return_list
389
-
390
- def finalize(self):
391
- """finalize script, disable editing"""
392
- self.status = "finalized"
393
- self.update_time_stamp()
394
-
395
- def save_as(self, name):
396
- """resave script, enable editing"""
397
- self.name = name
398
- self.status = "editing"
399
- self.update_time_stamp()
400
-
401
- def indent(self, unit=0):
402
- """helper: create _ unit of indent in code string"""
403
- string = "\n"
404
- for _ in range(unit):
405
- string += "\t"
406
- return string
407
-
408
- def convert_to_lines(self, exec_str_collection: dict):
409
- """
410
- Parse a dictionary of script functions and extract function body lines.
411
-
412
- :param exec_str_collection: Dictionary containing script types and corresponding function strings.
413
- :return: A dict containing script types as keys and lists of function body lines as values.
414
- """
415
- line_collection = {}
416
- for stype, func_str in exec_str_collection.items():
417
- if func_str:
418
- module = ast.parse(func_str)
419
- func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
420
-
421
- # Extract function body as source lines
422
- line_collection[stype] = [ast.unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
423
- # print(line_collection[stype])
424
- return line_collection
425
-
426
- def compile(self, script_path=None):
427
- """
428
- Compile the current script to a Python file.
429
- :return: String to write to a Python file.
430
- """
431
- self.sort_actions()
432
- run_name = self.name if self.name else "untitled"
433
- run_name = self.validate_function_name(run_name)
434
- exec_str_collection = {}
435
-
436
- for i in self.stypes:
437
- if self.script_dict[i]:
438
- func_str = self._generate_function_header(run_name, i) + self._generate_function_body(i)
439
- exec_str_collection[i] = func_str
440
- if script_path:
441
- self._write_to_file(script_path, run_name, exec_str_collection)
442
-
443
- return exec_str_collection
444
-
445
-
446
-
447
- @staticmethod
448
- def validate_function_name(name):
449
- """Replace invalid characters with underscores"""
450
- name = re.sub(r'\W|^(?=\d)', '_', name)
451
- # Check if it's a Python keyword and adjust if necessary
452
- if keyword.iskeyword(name):
453
- name += '_'
454
- return name
455
-
456
- def _generate_function_header(self, run_name, stype):
457
- """
458
- Generate the function header.
459
- """
460
- configure, config_type = self.config(stype)
461
-
462
- configure = [param + f":{param_type}" if not param_type == "any" else "" for param, param_type in
463
- config_type.items()]
464
-
465
- script_type = f"_{stype}" if stype != "script" else ""
466
- function_header = f"def {run_name}{script_type}("
467
-
468
- if stype == "script":
469
- function_header += ", ".join(configure)
470
-
471
- function_header += "):"
472
- # function_header += self.indent(1) + f"global {run_name}_{stype}"
473
- return function_header
474
-
475
- def _generate_function_body(self, stype):
476
- """
477
- Generate the function body for each type in stypes.
478
- """
479
- body = ''
480
- indent_unit = 1
481
-
482
- for index, action in enumerate(self.script_dict[stype]):
483
- text, indent_unit = self._process_action(indent_unit, action, index, stype)
484
- body += text
485
- return_str, return_list = self.config_return()
486
- if return_list and stype == "script":
487
- body += self.indent(indent_unit) + return_str
488
- return body
489
-
490
- def _process_action(self, indent_unit, action, index, stype):
491
- """
492
- Process each action within the script dictionary.
493
- """
494
- instrument = action['instrument']
495
- statement = action['args'].get('statement')
496
- args = self._process_args(action['args'])
497
-
498
- save_data = action['return']
499
- action_name = action['action']
500
- next_action = self._get_next_action(stype, index)
501
- # print(args)
502
- if instrument == 'if':
503
- return self._process_if(indent_unit, action_name, statement, next_action)
504
- elif instrument == 'while':
505
- return self._process_while(indent_unit, action_name, statement, next_action)
506
- elif instrument == 'variable':
507
- return self.indent(indent_unit) + f"{action_name} = {statement}", indent_unit
508
- elif instrument == 'wait':
509
- return f"{self.indent(indent_unit)}time.sleep({statement})", indent_unit
510
- elif instrument == 'repeat':
511
- return self._process_repeat(indent_unit, action_name, statement, next_action)
512
- #todo
513
- # elif instrument == 'registered_workflows':
514
- # return inspect.getsource(my_function)
515
- else:
516
- return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data)
517
-
518
- def _process_args(self, args):
519
- """
520
- Process arguments, handling any specific formatting needs.
521
- """
522
- if isinstance(args, str) and args.startswith("#"):
523
- return args[1:]
524
- return args
525
-
526
- def _process_if(self, indent_unit, action, args, next_action):
527
- """
528
- Process 'if' and 'else' actions.
529
- """
530
- exec_string = ""
531
- if action == 'if':
532
- exec_string += self.indent(indent_unit) + f"if {args}:"
533
- indent_unit += 1
534
- if next_action and next_action['instrument'] == 'if' and next_action['action'] == 'else':
535
- exec_string += self.indent(indent_unit) + "pass"
536
- # else:
537
-
538
- elif action == 'else':
539
- indent_unit -= 1
540
- exec_string += self.indent(indent_unit) + "else:"
541
- indent_unit += 1
542
- if next_action and next_action['instrument'] == 'if' and next_action['action'] == 'endif':
543
- exec_string += self.indent(indent_unit) + "pass"
544
- else:
545
- indent_unit -= 1
546
- return exec_string, indent_unit
547
-
548
- def _process_while(self, indent_unit, action, args, next_action):
549
- """
550
- Process 'while' and 'endwhile' actions.
551
- """
552
- exec_string = ""
553
- if action == 'while':
554
- exec_string += self.indent(indent_unit) + f"while {args}:"
555
- indent_unit += 1
556
- if next_action and next_action['instrument'] == 'while':
557
- exec_string += self.indent(indent_unit) + "pass"
558
- elif action == 'endwhile':
559
- indent_unit -= 1
560
- return exec_string, indent_unit
561
-
562
- def _process_repeat(self, indent_unit, action, args, next_action):
563
- """
564
- Process 'while' and 'endwhile' actions.
565
- """
566
- exec_string = ""
567
- if action == 'repeat':
568
- exec_string += self.indent(indent_unit) + f"for _ in range({args}):"
569
- indent_unit += 1
570
- if next_action and next_action['instrument'] == 'repeat':
571
- exec_string += self.indent(indent_unit) + "pass"
572
- elif action == 'endrepeat':
573
- indent_unit -= 1
574
- return exec_string, indent_unit
575
-
576
- def _process_instrument_action(self, indent_unit, instrument, action, args, save_data):
577
- """
578
- Process actions related to instruments.
579
- """
580
-
581
- if isinstance(args, dict):
582
- args_str = self._process_dict_args(args)
583
- single_line = f"{instrument}.{action}(**{args_str})"
584
- elif isinstance(args, str):
585
- single_line = f"{instrument}.{action} = {args}"
586
- else:
587
- single_line = f"{instrument}.{action}()"
588
-
589
- if save_data:
590
- save_data += " = "
591
-
592
- return self.indent(indent_unit) + save_data + single_line, indent_unit
593
-
594
- def _process_dict_args(self, args):
595
- """
596
- Process dictionary arguments, handling special cases like variables.
597
- """
598
- args_str = args.__str__()
599
- for arg in args:
600
- if isinstance(args[arg], str) and args[arg].startswith("#"):
601
- args_str = args_str.replace(f"'#{args[arg][1:]}'", args[arg][1:])
602
- elif isinstance(args[arg], dict):
603
- # print(args[arg])
604
- variables = self.get_variables()
605
- value = next(iter(args[arg]))
606
- if value not in variables:
607
- raise ValueError(f"Variable ({value}) is not defined.")
608
- args_str = args_str.replace(f"{args[arg]}", next(iter(args[arg])))
609
- # elif self._is_variable(arg):
610
- # print("is variable")
611
- # args_str = args_str.replace(f"'{args[arg]}'", args[arg])
612
- return args_str
613
-
614
- def _get_next_action(self, stype, index):
615
- """
616
- Get the next action in the sequence if it exists.
617
- """
618
- if index < (len(self.script_dict[stype]) - 1):
619
- return self.script_dict[stype][index + 1]
620
- return None
621
-
622
- def _is_variable(self, arg):
623
- """
624
- Check if the argument is of type 'variable'.
625
- """
626
- return arg in self.script_dict and self.script_dict[arg].get("arg_types") == "variable"
627
-
628
- def _write_to_file(self, script_path, run_name, exec_string):
629
- """
630
- Write the compiled script to a file.
631
- """
632
- with open(script_path + run_name + ".py", "w") as s:
633
- if self.deck:
634
- s.write(f"import {self.deck} as deck")
635
- else:
636
- s.write("deck = None")
637
- s.write("\nimport time")
638
- for i in exec_string.values():
639
- s.write(f"\n\n\n{i}")
640
-
641
- class WorkflowRun(db.Model):
642
- __tablename__ = 'workflow_runs'
643
-
644
- id = db.Column(db.Integer, primary_key=True)
645
- name = db.Column(db.String(128), nullable=False)
646
- platform = db.Column(db.String(128), nullable=False)
647
- start_time = db.Column(db.DateTime, default=datetime.now())
648
- end_time = db.Column(db.DateTime)
649
- data_path = db.Column(db.String(256))
650
- steps = db.relationship(
651
- 'WorkflowStep',
652
- backref='workflow_runs',
653
- cascade='all, delete-orphan',
654
- passive_deletes=True
655
- )
656
- def as_dict(self):
657
- dict = self.__dict__
658
- dict.pop('_sa_instance_state', None)
659
- return dict
660
-
661
- class WorkflowStep(db.Model):
662
- __tablename__ = 'workflow_steps'
663
-
664
- id = db.Column(db.Integer, primary_key=True)
665
- workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=False)
666
-
667
- phase = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
668
- repeat_index = db.Column(db.Integer, default=0) # Only applies to 'main' phase
669
- step_index = db.Column(db.Integer, default=0)
670
- method_name = db.Column(db.String(128), nullable=False)
671
- start_time = db.Column(db.DateTime)
672
- end_time = db.Column(db.DateTime)
673
- run_error = db.Column(db.Boolean, default=False)
674
-
675
- def as_dict(self):
676
- dict = self.__dict__.copy()
677
- dict.pop('_sa_instance_state', None)
678
- return dict
679
-
680
-
681
- class SingleStep(db.Model):
682
- __tablename__ = 'single_steps'
683
-
684
- id = db.Column(db.Integer, primary_key=True)
685
- method_name = db.Column(db.String(128), nullable=False)
686
- kwargs = db.Column(JSONType, nullable=False)
687
- start_time = db.Column(db.DateTime)
688
- end_time = db.Column(db.DateTime)
689
- run_error = db.Column(db.String(128))
690
- output = db.Column(JSONType)
691
-
692
- def as_dict(self):
693
- dict = self.__dict__.copy()
694
- dict.pop('_sa_instance_state', None)
695
- return dict
696
-
697
- if __name__ == "__main__":
698
- a = Script()
699
-
700
- print("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/form.py DELETED
@@ -1,560 +0,0 @@
1
- from enum import Enum
2
- from typing import get_origin, get_args, Union, Any
3
-
4
- from wtforms.fields.choices import SelectField
5
- from wtforms.fields.core import Field
6
- from wtforms.validators import InputRequired, ValidationError, Optional
7
- from wtforms.widgets.core import TextInput
8
-
9
- from flask_wtf import FlaskForm
10
- from wtforms import StringField, FloatField, HiddenField, BooleanField, IntegerField
11
- import inspect
12
-
13
- from ivoryos.utils.db_models import Script
14
- from ivoryos.utils.global_config import GlobalConfig
15
-
16
- global_config = GlobalConfig()
17
-
18
- def find_variable(data, script):
19
- """
20
- find user defined variables and return values in the script:Script
21
- :param data: string of input variable name
22
- :param script:Script object
23
- """
24
- variables: dict[str, str] = script.get_variables()
25
- for variable_name, variable_type in variables.items():
26
- if variable_name == data:
27
- return data, variable_type # variable_type int float str or "function_output"
28
- return None, None
29
-
30
-
31
- class VariableOrStringField(Field):
32
- widget = TextInput()
33
-
34
- def __init__(self, label='', validators=None, script=None, **kwargs):
35
- super(VariableOrStringField, self).__init__(label, validators, **kwargs)
36
- self.script = script
37
-
38
- def process_formdata(self, valuelist):
39
- if valuelist:
40
- if not self.script.editing_type == "script" and valuelist[0].startswith("#"):
41
- raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
42
- self.data = valuelist[0]
43
-
44
- def _value(self):
45
- if self.script:
46
- variable, variable_type = find_variable(self.data, self.script)
47
- if variable:
48
- return variable
49
-
50
- return str(self.data) if self.data is not None else ""
51
-
52
-
53
- class VariableOrFloatField(Field):
54
- widget = TextInput()
55
-
56
- def __init__(self, label='', validators=None, script=None, **kwargs):
57
- super(VariableOrFloatField, self).__init__(label, validators, **kwargs)
58
- self.script = script
59
-
60
- def _value(self):
61
- if self.script:
62
- variable, variable_type = find_variable(self.data, self.script)
63
- if variable:
64
- return variable
65
-
66
- if self.raw_data:
67
- return self.raw_data[0]
68
- if self.data is not None:
69
- return str(self.data)
70
- return ""
71
-
72
- def process_formdata(self, valuelist):
73
- if not valuelist:
74
- return
75
- elif valuelist[0].startswith("#"):
76
- if not self.script.editing_type == "script":
77
- raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
78
- self.data = valuelist[0]
79
- return
80
- try:
81
- if self.script:
82
- try:
83
- variable, variable_type = find_variable(valuelist[0], self.script)
84
- if variable:
85
- if not variable_type == "function_output":
86
- if variable_type not in ["float", "int"]:
87
- raise ValueError("Variable is not a valid float")
88
- self.data = variable
89
- return
90
- except ValueError:
91
- pass
92
- self.data = float(valuelist[0])
93
- except ValueError as exc:
94
- self.data = None
95
- raise ValueError(self.gettext("Not a valid float value.")) from exc
96
-
97
-
98
- # unset_value = UnsetValue()
99
-
100
-
101
- class VariableOrIntField(Field):
102
- widget = TextInput()
103
-
104
- def __init__(self, label='', validators=None, script=None, **kwargs):
105
- super(VariableOrIntField, self).__init__(label, validators, **kwargs)
106
- self.script = script
107
-
108
- def _value(self):
109
- if self.script:
110
- variable, variable_type = find_variable(self.data, self.script)
111
- if variable:
112
- return variable
113
-
114
- if self.raw_data:
115
- return self.raw_data[0]
116
- if self.data is not None:
117
- return str(self.data)
118
- return ""
119
-
120
- def process_formdata(self, valuelist):
121
- if not valuelist:
122
- return
123
- if self.script:
124
- variable, variable_type = find_variable(valuelist[0], self.script)
125
- if variable:
126
- try:
127
- if not variable_type == "function_output":
128
- if not variable_type == "int":
129
- raise ValueError("Not a valid integer value")
130
- self.data = str(variable)
131
- return
132
- except ValueError:
133
- pass
134
- if valuelist[0].startswith("#"):
135
- if not self.script.editing_type == "script":
136
- raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
137
- self.data = valuelist[0]
138
- return
139
- try:
140
- self.data = int(valuelist[0])
141
- except ValueError as exc:
142
- self.data = None
143
- raise ValueError(self.gettext("Not a valid integer value.")) from exc
144
-
145
-
146
- class VariableOrBoolField(BooleanField):
147
- widget = TextInput()
148
- false_values = (False, "false", "", "False", "f", "F")
149
-
150
- def __init__(self, label='', validators=None, script=None, **kwargs):
151
- super(VariableOrBoolField, self).__init__(label, validators, **kwargs)
152
- self.script = script
153
-
154
- def process_data(self, value):
155
-
156
- if self.script:
157
- variable, variable_type = find_variable(value, self.script)
158
- if variable:
159
- if not variable_type == "function_output":
160
- raise ValueError("Not accepting boolean variables")
161
- return variable
162
-
163
- self.data = bool(value)
164
-
165
- def process_formdata(self, valuelist):
166
- # todo
167
- # print(valuelist)
168
- if not valuelist or not type(valuelist) is list:
169
- self.data = False
170
- else:
171
- value = valuelist[0] if type(valuelist) is list else valuelist
172
- if value.startswith("#"):
173
- if not self.script.editing_type == "script":
174
- raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
175
- self.data = valuelist[0]
176
- elif value in self.false_values:
177
- self.data = False
178
- else:
179
- self.data = True
180
-
181
- def _value(self):
182
-
183
- if self.script:
184
- variable, variable_type = find_variable(self.raw_data, self.script)
185
- if variable:
186
- return variable
187
-
188
- if self.raw_data:
189
- return str(self.raw_data[0])
190
- return "y"
191
-
192
-
193
- class FlexibleEnumField(StringField):
194
- def __init__(self, label=None, validators=None, choices=None, script=None, **kwargs):
195
- super().__init__(label, validators, **kwargs)
196
- self.script = script
197
- self.enum_class = choices
198
- self.choices = [e.name for e in self.enum_class]
199
- # self.value_list = [e.name for e in self.enum_class]
200
-
201
-
202
- def process_formdata(self, valuelist):
203
- if valuelist:
204
- key = valuelist[0]
205
- if key in self.choices:
206
- # Convert the string key to Enum instance
207
- self.data = self.enum_class[key].value
208
- elif self.data.startswith("#"):
209
- if not self.script.editing_type == "script":
210
- raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
211
- self.data = self.data
212
- else:
213
- raise ValidationError(
214
- f"Invalid choice: '{key}'. Must match one of {list(self.enum_class.__members__.keys())}")
215
-
216
-
217
- def format_name(name):
218
- """Converts 'example_name' to 'Example Name'."""
219
- name = name.split(".")[-1]
220
- text = ' '.join(word for word in name.split('_'))
221
- return text.capitalize()
222
-
223
- def parse_annotation(annotation):
224
- """
225
- Given a type annotation, return:
226
- - a list of all valid types (excluding NoneType)
227
- - a boolean indicating if the value can be None (optional)
228
- """
229
- origin = get_origin(annotation)
230
- args = get_args(annotation)
231
-
232
- if annotation is Any:
233
- return [str], True # fallback: accept any string, optional
234
-
235
- if origin is Union:
236
- types = list(set(args))
237
- is_optional = type(None) in types
238
- non_none_types = [t for t in types if t is not type(None)]
239
- return non_none_types, is_optional
240
-
241
- # Not a Union, just a regular type
242
- return [annotation], False
243
-
244
- def create_form_for_method(method, autofill, script=None, design=True):
245
- """
246
- Create forms for each method or signature
247
- :param method: dict(docstring, signature)
248
- :param autofill:bool if autofill is enabled
249
- :param script:Script object
250
- :param design: if design is enabled
251
- """
252
-
253
- class DynamicForm(FlaskForm):
254
- pass
255
-
256
- annotation_mapping = {
257
- int: (VariableOrIntField if design else IntegerField, 'Enter integer value'),
258
- float: (VariableOrFloatField if design else FloatField, 'Enter numeric value'),
259
- str: (VariableOrStringField if design else StringField, 'Enter text'),
260
- bool: (VariableOrBoolField if design else BooleanField, 'Empty for false')
261
- }
262
- sig = method if type(method) is inspect.Signature else inspect.signature(method)
263
-
264
- for param in sig.parameters.values():
265
- if param.name == 'self':
266
- continue
267
- formatted_param_name = format_name(param.name)
268
-
269
- default_value = None
270
- if autofill:
271
- default_value = f'#{param.name}'
272
- else:
273
- if param.default is not param.empty:
274
- if isinstance(param.default, Enum):
275
- default_value = param.default.name
276
- else:
277
- default_value = param.default
278
-
279
- field_kwargs = {
280
- "label": formatted_param_name,
281
- "default": default_value,
282
- "validators": [InputRequired()] if param.default is param.empty else [Optional()],
283
- **({"script": script} if (autofill or design) else {})
284
- }
285
- if isinstance(param.annotation, type) and issubclass(param.annotation, Enum):
286
- # enum_class = [(e.name, e.value) for e in param.annotation]
287
- field_class = FlexibleEnumField
288
- placeholder_text = f"Choose or type a value for {param.annotation.__name__} (start with # for custom)"
289
- extra_kwargs = {"choices": param.annotation}
290
- else:
291
- # print(param.annotation)
292
- annotation, optional = parse_annotation(param.annotation)
293
- annotation = annotation[0]
294
- field_class, placeholder_text = annotation_mapping.get(
295
- annotation,
296
- (VariableOrStringField if design else StringField, f'Enter {param.annotation} value')
297
- )
298
- extra_kwargs = {}
299
- if optional:
300
- field_kwargs["filters"] = [lambda x: x if x != '' else None]
301
-
302
-
303
- render_kwargs = {"placeholder": placeholder_text}
304
-
305
- # Create the field with additional rendering kwargs for placeholder text
306
- field = field_class(**field_kwargs, render_kw=render_kwargs, **extra_kwargs)
307
- setattr(DynamicForm, param.name, field)
308
-
309
- # setattr(DynamicForm, f'add', fname)
310
- return DynamicForm
311
-
312
-
313
- def create_add_form(attr, attr_name, autofill: bool, script=None, design: bool = True):
314
- """
315
- Create forms for each method or signature
316
- :param attr: dict(docstring, signature)
317
- :param attr_name: method name
318
- :param autofill:bool if autofill is enabled
319
- :param script:Script object
320
- :param design: if design is enabled. Design allows string input for parameter names ("#param") for all fields
321
- """
322
- signature = attr.get('signature', {})
323
- docstring = attr.get('docstring', "")
324
- # print(signature, docstring)
325
- dynamic_form = create_form_for_method(signature, autofill, script, design)
326
- if design:
327
- return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
328
- setattr(dynamic_form, 'return', return_value)
329
- hidden_method_name = HiddenField(name=f'hidden_name', description=docstring, render_kw={"value": f'{attr_name}'})
330
- setattr(dynamic_form, 'hidden_name', hidden_method_name)
331
- return dynamic_form
332
-
333
-
334
- def create_form_from_module(sdl_module, autofill: bool = False, script=None, design: bool = False):
335
- """
336
- Create forms for each method, used for control routes
337
- :param sdl_module: method module
338
- :param autofill:bool if autofill is enabled
339
- :param script:Script object
340
- :param design: if design is enabled
341
- """
342
- method_forms = {}
343
- for attr_name in dir(sdl_module):
344
- method = getattr(sdl_module, attr_name)
345
- if inspect.ismethod(method) and not attr_name.startswith('_'):
346
- signature = inspect.signature(method)
347
- docstring = inspect.getdoc(method)
348
- attr = dict(signature=signature, docstring=docstring)
349
- form_class = create_add_form(attr, attr_name, autofill, script, design)
350
- method_forms[attr_name] = form_class()
351
- return method_forms
352
-
353
-
354
- def create_form_from_pseudo(pseudo: dict, autofill: bool, script=None, design=True):
355
- """
356
- Create forms for pseudo method, used for design routes
357
- :param pseudo:{'dose_liquid': {
358
- "docstring": "some docstring",
359
- "signature": Signature(amount_in_ml: float, rate_ml_per_minute: float) }
360
- }
361
- :param autofill:bool if autofill is enabled
362
- :param script:Script object
363
- :param design: if design is enabled
364
- """
365
- method_forms = {}
366
- for attr_name, signature in pseudo.items():
367
- # signature = info.get('signature', {})
368
- form_class = create_add_form(signature, attr_name, autofill, script, design)
369
- method_forms[attr_name] = form_class()
370
- return method_forms
371
-
372
-
373
- def create_form_from_action(action: dict, script=None, design=True):
374
- '''
375
- Create forms for single action, used for design routes
376
- :param action: {'action': 'dose_solid', 'arg_types': {'amount_in_mg': 'float', 'bring_in': 'bool'},
377
- 'args': {'amount_in_mg': 5.0, 'bring_in': False}, 'id': 9,
378
- 'instrument': 'deck.sdl', 'return': '', 'uuid': 266929188668995}
379
- :param script:Script object
380
- :param design: if design is enabled
381
-
382
- '''
383
-
384
- arg_types = action.get("arg_types", {})
385
- args = action.get("args", {})
386
- save_as = action.get("return")
387
-
388
- class DynamicForm(FlaskForm):
389
- pass
390
-
391
- annotation_mapping = {
392
- "int": (VariableOrIntField if design else IntegerField, 'Enter integer value'),
393
- "float": (VariableOrFloatField if design else FloatField, 'Enter numeric value'),
394
- "str": (VariableOrStringField if design else StringField, 'Enter text'),
395
- "bool": (VariableOrBoolField if design else BooleanField, 'Empty for false')
396
- }
397
-
398
- for name, param_type in arg_types.items():
399
- formatted_param_name = format_name(name)
400
- value = args.get(name, "")
401
- if type(value) is dict:
402
- value = next(iter(value))
403
- field_kwargs = {
404
- "label": formatted_param_name,
405
- "default": f'{value}',
406
- "validators": [InputRequired()],
407
- **({"script": script})
408
- }
409
- param_type = param_type if type(param_type) is str else f"{param_type}"
410
- field_class, placeholder_text = annotation_mapping.get(
411
- param_type,
412
- (VariableOrStringField if design else StringField, f'Enter {param_type} value')
413
- )
414
- render_kwargs = {"placeholder": placeholder_text}
415
-
416
- # Create the field with additional rendering kwargs for placeholder text
417
- field = field_class(**field_kwargs, render_kw=render_kwargs)
418
- setattr(DynamicForm, name, field)
419
-
420
- if design:
421
- return_value = StringField(label='Save value as', default=f"{save_as}", render_kw={"placeholder": "Optional"})
422
- setattr(DynamicForm, 'return', return_value)
423
- return DynamicForm()
424
-
425
- def create_all_builtin_forms(script):
426
- all_builtin_forms = {}
427
- for logic_name in ['if', 'while', 'variable', 'wait', 'repeat']:
428
- # signature = info.get('signature', {})
429
- form_class = create_builtin_form(logic_name, script)
430
- all_builtin_forms[logic_name] = form_class()
431
- return all_builtin_forms
432
-
433
- def create_builtin_form(logic_type, script):
434
- """
435
- Create a builtin form {if, while, variable, repeat, wait}
436
- """
437
- class BuiltinFunctionForm(FlaskForm):
438
- pass
439
-
440
- placeholder_text = {
441
- 'wait': 'Enter second',
442
- 'repeat': 'Enter an integer'
443
- }.get(logic_type, 'Enter statement')
444
- description_text = {
445
- 'variable': 'Your variable can be numbers, boolean (True or False) or text ("text")',
446
- }.get(logic_type, '')
447
- field_class = {
448
- 'wait': VariableOrFloatField,
449
- 'repeat': VariableOrIntField
450
- }.get(logic_type, VariableOrStringField) # Default to StringField as a fallback
451
- field_kwargs = {
452
- "label": f'statement',
453
- "validators": [InputRequired()] if logic_type in ['wait', "variable"] else [],
454
- "description": description_text,
455
- "script": script
456
- }
457
- render_kwargs = {"placeholder": placeholder_text}
458
- field = field_class(**field_kwargs, render_kw=render_kwargs)
459
- setattr(BuiltinFunctionForm, "statement", field)
460
- if logic_type == 'variable':
461
- variable_field = StringField(label=f'variable', validators=[InputRequired()],
462
- description="Your variable name cannot include space",
463
- render_kw=render_kwargs)
464
- type_field = SelectField(
465
- 'Select Input Type',
466
- choices=[('int', 'Integer'), ('float', 'Float'), ('str', 'String'), ('bool', 'Boolean')],
467
- default='str' # Optional default value
468
- )
469
- setattr(BuiltinFunctionForm, "variable", variable_field)
470
- setattr(BuiltinFunctionForm, "type", type_field)
471
- hidden_field = HiddenField(name=f'builtin_name', render_kw={"value": f'{logic_type}'})
472
- setattr(BuiltinFunctionForm, "builtin_name", hidden_field)
473
- return BuiltinFunctionForm
474
-
475
-
476
- def get_method_from_workflow(function_string):
477
- """Creates a function from a string and assigns it a new name."""
478
-
479
- namespace = {}
480
- exec(function_string, globals(), namespace) # Execute the string in a safe namespace
481
- func_name = next(iter(namespace))
482
- # Get the function name dynamically
483
- return namespace[func_name]
484
-
485
-
486
- def create_workflow_forms(script, autofill: bool = False, design: bool = False):
487
- workflow_forms = {}
488
- functions = {}
489
- class RegisteredWorkflows:
490
- pass
491
-
492
- deck_name = script.deck
493
- workflows = Script.query.filter(Script.deck==deck_name, Script.name != script.name).all()
494
- for workflow in workflows:
495
- compiled_strs = workflow.compile().get('script', "")
496
- method = get_method_from_workflow(compiled_strs)
497
- functions[workflow.name] = dict(signature=inspect.signature(method), docstring=inspect.getdoc(method))
498
- setattr(RegisteredWorkflows, workflow.name, method)
499
-
500
- form_class = create_form_for_method(method, autofill, script, design)
501
-
502
- hidden_method_name = HiddenField(name=f'hidden_name', description="",
503
- render_kw={"value": f'{workflow.name}'})
504
- if design:
505
- return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
506
- setattr(form_class, 'return', return_value)
507
- setattr(form_class, 'workflow_name', hidden_method_name)
508
- workflow_forms[workflow.name] = form_class()
509
- global_config.registered_workflows = RegisteredWorkflows
510
- return workflow_forms, functions
511
-
512
-
513
- def create_action_button(script, stype=None):
514
- """
515
- Creates action buttons for design route (design canvas)
516
- :param script: Script object
517
- :param stype: script type (script, prep, cleanup)
518
- """
519
- stype = stype or script.editing_type
520
- variables = script.get_variables()
521
- return [_action_button(i, variables) for i in script.get_script(stype)]
522
-
523
-
524
- def _action_button(action: dict, variables: dict):
525
- """
526
- Creates action button for one action
527
- :param action: Action dict
528
- :param variables: created variable dict
529
- """
530
- style = {
531
- "repeat": "background-color: lightsteelblue",
532
- "if": "background-color: salmon",
533
- "while": "background-color: salmon",
534
- }.get(action['instrument'], "")
535
-
536
- if action['instrument'] in ['if', 'while', 'repeat']:
537
- text = f"{action['action']} {action['args'].get('statement', '')}"
538
- elif action['instrument'] == 'variable':
539
- text = f"{action['action']} = {action['args'].get('statement')}"
540
- else:
541
- # regular action button
542
- prefix = f"{action['return']} = " if action['return'] else ""
543
- action_text = f"{action['instrument'].split('.')[-1] if action['instrument'].startswith('deck') else action['instrument']}.{action['action']}"
544
- arg_string = ""
545
- if action['args']:
546
- if type(action['args']) is dict:
547
- arg_list = []
548
- for k, v in action['args'].items():
549
- if isinstance(v, dict):
550
- value = next(iter(v)) # Extract the first key if it's a dict
551
- # show warning color for variable calling when there is no definition
552
- style = "background-color: khaki" if value not in variables.keys() else ""
553
- else:
554
- value = v # Keep the original value if not a dict
555
- arg_list.append(f"{k} = {value}") # Format the key-value pair
556
- arg_string = "(" + ", ".join(arg_list) + ")"
557
- else:
558
- arg_string = f"= {action['args']}"
559
- text = f"{prefix}{action_text} {arg_string}"
560
- return dict(label=text, style=style, uuid=action["uuid"], id=action["id"], instrument=action['instrument'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/global_config.py DELETED
@@ -1,87 +0,0 @@
1
- import threading
2
-
3
-
4
- class GlobalConfig:
5
- _instance = None
6
-
7
- def __new__(cls, *args, **kwargs):
8
- if cls._instance is None:
9
- cls._instance = super(GlobalConfig, cls).__new__(cls, *args, **kwargs)
10
- cls._instance._deck = None
11
- cls._instance._registered_workflows = None
12
- cls._instance._agent = None
13
- cls._instance._defined_variables = {}
14
- cls._instance._api_variables = set()
15
- cls._instance._deck_snapshot = {}
16
- cls._instance._runner_lock = threading.Lock()
17
- cls._instance._runner_status = None
18
- return cls._instance
19
-
20
- @property
21
- def deck(self):
22
- return self._deck
23
-
24
- @deck.setter
25
- def deck(self, value):
26
- if self._deck is None:
27
- self._deck = value
28
-
29
- @property
30
- def registered_workflows(self):
31
- return self._registered_workflows
32
-
33
- @registered_workflows.setter
34
- def registered_workflows(self, value):
35
- if self._registered_workflows is None:
36
- self._registered_workflows = value
37
-
38
-
39
- @property
40
- def deck_snapshot(self):
41
- return self._deck_snapshot
42
-
43
- @deck_snapshot.setter
44
- def deck_snapshot(self, value):
45
- self._deck_snapshot = value
46
-
47
-
48
- @property
49
- def agent(self):
50
- return self._agent
51
-
52
- @agent.setter
53
- def agent(self, value):
54
- if self._agent is None:
55
- self._agent = value
56
-
57
- @property
58
- def defined_variables(self):
59
- return self._defined_variables
60
-
61
- @defined_variables.setter
62
- def defined_variables(self, value):
63
- self._defined_variables = value
64
-
65
- @property
66
- def api_variables(self):
67
- return self._api_variables
68
-
69
- @api_variables.setter
70
- def api_variables(self, value):
71
- self._api_variables = value
72
-
73
- @property
74
- def runner_lock(self):
75
- return self._runner_lock
76
-
77
- @runner_lock.setter
78
- def runner_lock(self, value):
79
- self._runner_lock = value
80
-
81
- @property
82
- def runner_status(self):
83
- return self._runner_status
84
-
85
- @runner_status.setter
86
- def runner_status(self, value):
87
- self._runner_status = value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/input_types.md DELETED
@@ -1,14 +0,0 @@
1
- ## Supported Input Types
2
- ### **Commonly Used Types**
3
- | Type | Example Input | Example Conversion |
4
- |---------|------------------------|--------------------------------------|
5
- | `int` | `"42"` | `42` |
6
- | `float` | `"3.14"` | `3.14` |
7
- | `str` | `"hello"` | `"hello"` |
8
- | `bool` | `"True"` / `"False"` | `True` / `False` |
9
- | `list ` | `"1,2,3"` | `[1, 2, 3]` |
10
- | `tuple` | `"1,hello,3.5"` | `(1, "hello", 3.5)` |
11
- | `set` | `"1,2,3"` | `{1, 2, 3}` |
12
- | `Any` | `"anything"` | `"anything"` (no conversion applied) |
13
-
14
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/llm_agent.py DELETED
@@ -1,183 +0,0 @@
1
- import inspect
2
- import json
3
- import os
4
- import re
5
-
6
- from openai import OpenAI
7
-
8
-
9
- # from dotenv import load_dotenv
10
- # load_dotenv()
11
-
12
- # host = "137.82.65.246"
13
- # model = "llama3"
14
-
15
- # structured output,
16
- # class Action(BaseModel):
17
- # action: str
18
- # args: dict
19
- # arg_types: dict
20
- #
21
- #
22
- # class ActionPlan(BaseModel):
23
- # actions: list[Action]
24
- # # final_answer: str
25
-
26
-
27
- class LlmAgent:
28
- def __init__(self, model="llama3", output_path=os.curdir, host=None):
29
- self.host = host
30
- self.base_url = f"http://{self.host}:11434/v1/" if host is not None else ""
31
- self.model = model
32
- self.output_path = os.path.join(output_path, "llm_output") if output_path is not None else None
33
- self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) if host is None else OpenAI(api_key="ollama",
34
- base_url=self.base_url)
35
- if self.output_path is not None:
36
- os.makedirs(self.output_path, exist_ok=True)
37
-
38
- @staticmethod
39
- def extract_annotations_docstrings(module_sigs):
40
- class_str = ""
41
-
42
- for name, value in module_sigs.items():
43
- signature = value.get("signature")
44
- docstring = value.get("docstring")
45
- class_str += f'\tdef {name}{signature}:\n'
46
- class_str += f'\t\t"""\n\t\t{docstring}\n\t\t"""' + '\n' if docstring else ''
47
- class_str = class_str.replace('self, ', '')
48
- class_str = class_str.replace('self', '')
49
- name_list = list(module_sigs.keys())
50
- # print(class_str)
51
- # with open(os.path.join(self.output_path, "docstring_manual.txt"), "w") as f:
52
- # f.write(class_str)
53
- return class_str, name_list
54
-
55
- @staticmethod
56
- def parse_code_from_msg(msg):
57
- msg = msg.strip()
58
- # print(msg)
59
- # code_blocks = re.findall(r'```(?:json\s)?(.*?)```', msg, re.DOTALL)
60
- code_blocks = re.findall(r'\[\s*\{.*?\}\s*\]', msg, re.DOTALL)
61
-
62
- json_blocks = []
63
- for block in code_blocks:
64
- if not block.startswith('['):
65
- start_index = block.find('[')
66
- block = block[start_index:]
67
- block = re.sub(r'//.*', '', block)
68
- block = block.replace('True', 'true').replace('False', 'false')
69
- try:
70
- # Try to parse the block as JSON
71
- json_data = json.loads(block.strip())
72
- if isinstance(json_data, list):
73
- json_blocks = json_data
74
- except json.JSONDecodeError:
75
- continue
76
- return json_blocks
77
-
78
- def _generate(self, robot_sigs, prompt):
79
- # deck_info, name_list = self.extract_annotations_docstrings(type(robot))
80
- deck_info, name_list = self.extract_annotations_docstrings(robot_sigs)
81
- full_prompt = '''I have some python functions, for example when calling them I want to write them using JSON,
82
- it is necessary to include all args
83
- for example
84
- def dose_solid(amount_in_mg:float, bring_in:bool=True): def analyze():
85
- dose_solid(3)
86
- analyze()
87
- I would want to write to
88
- [
89
- {
90
- "action": "dose_solid",
91
- "arg_types": {
92
- "amount_in_mg": "float",
93
- "bring_in": "bool"
94
- },
95
- "args": {
96
- "amount_in_mg": 3,
97
- "bring_in": true
98
- }
99
- },
100
- {
101
- "action": "analyze",
102
- "arg_types": {},
103
- "args": {}
104
- }
105
- ]
106
- ''' + f'''
107
- Now these are my callable functions,
108
- {deck_info}
109
- and I want you to find the most appropriate function if I want to do these tasks
110
- """{prompt}"""
111
- ,and write a list of dictionary in json accordingly. Please only use these action names {name_list},
112
- can you also help find the default value you can't find the info from my request.
113
- '''
114
- if self.output_path is not None:
115
- with open(os.path.join(self.output_path, "prompt.txt"), "w") as f:
116
- f.write(full_prompt)
117
- messages = [{"role": "user",
118
- "content": full_prompt}, ]
119
- # if self.host == "openai":
120
- output = self.client.chat.completions.create(
121
- messages=messages,
122
- model=self.model,
123
- # response_format={"type": "json_object"},
124
- )
125
- msg = output.choices[0].message.content
126
- # msg = output.choices[0].message.parsed
127
-
128
- code = self.parse_code_from_msg(msg)
129
- code = [action for action in code if action.get('action', '') in name_list]
130
- # print('\033[91m', code, '\033[0m')
131
- return code
132
-
133
- def generate_code(self, robot_signature, prompt, attempt_allowance: int = 3):
134
- attempt = 0
135
-
136
- while attempt < attempt_allowance:
137
- _code = self._generate(robot_signature, prompt)
138
- attempt += 1
139
- if _code:
140
- break
141
-
142
- return self.fill_blanks(_code, robot_signature)
143
- # return code
144
-
145
- @staticmethod
146
- def fill_blanks(actions, robot_signature):
147
- for action in actions:
148
- action_name = action['action']
149
- action_signature = robot_signature.get(action_name).get('signature', {})
150
- args = action.get("args", {})
151
- arg_types = action.get("arg_types", {})
152
- for param in action_signature.parameters.values():
153
- if param.name == 'self':
154
- continue
155
- if param.name not in args:
156
- args[param.name] = param.default if param.default is not param.empty else ''
157
- arg_types[param.name] = param.annotation.__name__
158
- action['args'] = args
159
- action['arg_types'] = arg_types
160
- return actions
161
-
162
-
163
- if __name__ == "__main__":
164
- from pprint import pprint
165
- from example.abstract_sdl_example.abstract_sdl import deck
166
-
167
- from utils import parse_functions
168
-
169
- deck_sig = parse_functions(deck, doc_string=True)
170
- # llm_agent = LlmAgent(host="openai", model="gpt-3.5-turbo")
171
- llm_agent = LlmAgent(host="localhost", model="llama3.1")
172
- # robot = IrohDeck()
173
- # extract_annotations_docstrings(DummySDLDeck)
174
- prompt = '''I want to start with dosing 10 mg of current sample, and add 1 mL of toluene
175
- and equilibrate for 10 minute at 40 degrees, then sample 20 ul of sample to analyze with hplc, and save result'''
176
- code = llm_agent.generate_code(deck_sig, prompt)
177
- pprint(code)
178
-
179
- """
180
- I want to dose 10mg, 6mg, 4mg, 3mg, 2mg, 1mg to 6 vials
181
- I want to add 10 mg to vial a3, and 10 ml of liquid, then shake them for 3 minutes
182
-
183
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/script_runner.py DELETED
@@ -1,336 +0,0 @@
1
- import ast
2
- import os
3
- import csv
4
- import threading
5
- import time
6
- from datetime import datetime
7
-
8
- from ivoryos.utils import utils, bo_campaign
9
- from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep, db, SingleStep
10
- from ivoryos.utils.global_config import GlobalConfig
11
-
12
- global_config = GlobalConfig()
13
- global deck
14
- deck = None
15
- # global deck, registered_workflows
16
- # deck, registered_workflows = None, None
17
-
18
- class ScriptRunner:
19
- def __init__(self, globals_dict=None):
20
- self.retry = False
21
- if globals_dict is None:
22
- globals_dict = globals()
23
- self.globals_dict = globals_dict
24
- self.pause_event = threading.Event() # A threading event to manage pause/resume
25
- self.pause_event.set()
26
- self.stop_pending_event = threading.Event()
27
- self.stop_current_event = threading.Event()
28
- self.is_running = False
29
- self.lock = global_config.runner_lock
30
- self.paused = False
31
- self.current_app = None
32
-
33
- def toggle_pause(self):
34
- """Toggles between pausing and resuming the script"""
35
- self.paused = not self.paused
36
- if self.pause_event.is_set():
37
- self.pause_event.clear() # Pause the script
38
- return "Paused"
39
- else:
40
- self.pause_event.set() # Resume the script
41
- return "Resumed"
42
-
43
- def pause_status(self):
44
- """Toggles between pausing and resuming the script"""
45
- return self.paused
46
-
47
- def reset_stop_event(self):
48
- """Resets the stop event"""
49
- self.stop_pending_event.clear()
50
- self.stop_current_event.clear()
51
- self.pause_event.set()
52
-
53
- def abort_pending(self):
54
- """Abort the pending iteration after the current is finished"""
55
- self.stop_pending_event.set()
56
- # print("Stop pending tasks")
57
-
58
- def stop_execution(self):
59
- """Force stop everything, including ongoing tasks."""
60
- self.stop_current_event.set()
61
- self.abort_pending()
62
-
63
-
64
- def run_script(self, script, repeat_count=1, run_name=None, logger=None, socketio=None, config=None, bo_args=None,
65
- output_path="", compiled=False, current_app=None):
66
- global deck
67
- if deck is None:
68
- deck = global_config.deck
69
-
70
- if self.current_app is None:
71
- self.current_app = current_app
72
- # time.sleep(1) # Optional: may help ensure deck readiness
73
-
74
- # Try to acquire lock without blocking
75
- if not self.lock.acquire(blocking=False):
76
- if logger:
77
- logger.info("System is busy. Please wait for it to finish or stop it before starting a new one.")
78
- return None
79
-
80
- self.reset_stop_event()
81
-
82
- thread = threading.Thread(
83
- target=self._run_with_stop_check,
84
- args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path, current_app, compiled)
85
- )
86
- thread.start()
87
- return thread
88
-
89
- def exec_steps(self, script, section_name, logger, socketio, run_id, i_progress, **kwargs):
90
- """
91
- Executes a function defined in a string line by line
92
- :param func_str: The function as a string
93
- :param kwargs: Arguments to pass to the function
94
- :return: The final result of the function execution
95
- """
96
- _func_str = script.python_script or script.compile()
97
- step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
98
- global deck
99
- # global deck, registered_workflows
100
- if deck is None:
101
- deck = global_config.deck
102
- # if registered_workflows is None:
103
- # registered_workflows = global_config.registered_workflows
104
-
105
- # for i, line in enumerate(step_list):
106
- # if line.startswith("registered_workflows"):
107
- #
108
- # func_str = script.compile()
109
- # Parse function body from string
110
- temp_connections = global_config.defined_variables
111
- # Prepare execution environment
112
- exec_globals = {"deck": deck, "time":time} # Add required global objects
113
- # exec_globals = {"deck": deck, "time": time, "registered_workflows":registered_workflows} # Add required global objects
114
- exec_globals.update(temp_connections)
115
- exec_locals = {} # Local execution scope
116
-
117
- # Define function arguments manually in exec_locals
118
- exec_locals.update(kwargs)
119
- index = 0
120
-
121
- # Execute each line dynamically
122
- while index < len(step_list):
123
- if self.stop_current_event.is_set():
124
- logger.info(f'Stopping execution during {section_name}')
125
- step = WorkflowStep(
126
- workflow_id=run_id,
127
- phase=section_name,
128
- repeat_index=i_progress,
129
- step_index=index,
130
- method_name="stop",
131
- start_time=datetime.now(),
132
- end_time=datetime.now(),
133
- run_error=False,
134
- )
135
- db.session.add(step)
136
- break
137
- line = step_list[index]
138
- method_name = line.strip().split("(")[0] if "(" in line else line.strip()
139
- start_time = datetime.now()
140
- step = WorkflowStep(
141
- workflow_id=run_id,
142
- phase=section_name,
143
- repeat_index=i_progress,
144
- step_index=index,
145
- method_name=method_name,
146
- start_time=start_time,
147
- )
148
- db.session.add(step)
149
- db.session.commit()
150
- logger.info(f"Executing: {line}")
151
- socketio.emit('execution', {'section': f"{section_name}-{index}"})
152
- # self._emit_progress(socketio, 100)
153
- # if line.startswith("registered_workflows"):
154
- # line = line.replace("registered_workflows.", "")
155
- try:
156
- if line.startswith("time.sleep("): # add safe sleep for time.sleep lines
157
- duration_str = line[len("time.sleep("):-1]
158
- duration = float(duration_str)
159
- self.safe_sleep(duration)
160
- else:
161
- exec(line, exec_globals, exec_locals)
162
- step.run_error = False
163
- except Exception as e:
164
- logger.error(f"Error during script execution: {e}")
165
- socketio.emit('error', {'message': str(e)})
166
-
167
- step.run_error = True
168
- self.toggle_pause()
169
- step.end_time = datetime.now()
170
- # db.session.add(step)
171
- db.session.commit()
172
-
173
- self.pause_event.wait()
174
-
175
- # todo update script during the run
176
- # _func_str = script.compile()
177
- # step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
178
- if not step.run_error:
179
- index += 1
180
- elif not self.retry:
181
- index += 1
182
- return exec_locals # Return the 'results' variable
183
-
184
- def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
185
- output_path, current_app, compiled):
186
- time.sleep(1)
187
- # _func_str = script.compile()
188
- # step_list_dict: dict = script.convert_to_lines(_func_str)
189
- self._emit_progress(socketio, 1)
190
-
191
- # Run "prep" section once
192
- script_dict = script.script_dict
193
- with current_app.app_context():
194
-
195
- run = WorkflowRun(name=script.name or "untitled", platform=script.deck or "deck",start_time=datetime.now())
196
- db.session.add(run)
197
- db.session.commit()
198
- run_id = run.id # Save the ID
199
- global_config.runner_status = {"id":run_id, "type": "workflow"}
200
- self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run_id)
201
- output_list = []
202
- _, arg_type = script.config("script")
203
- _, return_list = script.config_return()
204
-
205
- # Run "script" section multiple times
206
- if repeat_count:
207
- self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
208
- run_name, return_list, compiled, logger, socketio, run_id=run_id)
209
- elif config:
210
- self._run_config_section(config, arg_type, output_list, script, run_name, logger,
211
- socketio, run_id=run_id, compiled=compiled)
212
-
213
- # Run "cleanup" section once
214
- self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
215
- # Reset the running flag when done
216
- self.lock.release()
217
- # Save results if necessary
218
- filename = None
219
- if not script.python_script and output_list:
220
- filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
221
- self._emit_progress(socketio, 100)
222
- with current_app.app_context():
223
- run = db.session.get(WorkflowRun, run_id) # SQLAlchemy 1.4+ recommended method
224
- run.end_time = datetime.now()
225
- run.data_path = filename
226
- db.session.commit()
227
-
228
- def _run_actions(self, script, section_name="", logger=None, socketio=None, run_id=None):
229
- _func_str = script.python_script or script.compile()
230
- step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
231
- logger.info(f'Executing {section_name} steps') if step_list else logger.info(f'No {section_name} steps')
232
- if self.stop_pending_event.is_set():
233
- logger.info(f"Stopping execution during {section_name} section.")
234
- return
235
- if step_list:
236
- self.exec_steps(script, section_name, logger, socketio, run_id=run_id, i_progress=0)
237
-
238
- def _run_config_section(self, config, arg_type, output_list, script, run_name, logger, socketio, run_id, compiled=True):
239
- if not compiled:
240
- for i in config:
241
- try:
242
- i = utils.convert_config_type(i, arg_type)
243
- compiled = True
244
- except Exception as e:
245
- logger.info(e)
246
- compiled = False
247
- break
248
- if compiled:
249
- for i, kwargs in enumerate(config):
250
- kwargs = dict(kwargs)
251
- if self.stop_pending_event.is_set():
252
- logger.info(f'Stopping execution during {run_name}: {i + 1}/{len(config)}')
253
- break
254
- logger.info(f'Executing {i + 1} of {len(config)} with kwargs = {kwargs}')
255
- progress = (i + 1) * 100 / len(config)
256
- self._emit_progress(socketio, progress)
257
- # fname = f"{run_name}_script"
258
- # function = self.globals_dict[fname]
259
- output = self.exec_steps(script, "script", logger, socketio, run_id, i, **kwargs)
260
- if output:
261
- # kwargs.update(output)
262
- output_list.append(output)
263
-
264
- def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list, compiled,
265
- logger, socketio, run_id):
266
- if bo_args:
267
- logger.info('Initializing optimizer...')
268
- if compiled:
269
- ax_client = bo_campaign.ax_init_opc(bo_args)
270
- else:
271
- ax_client = bo_campaign.ax_init_form(bo_args, arg_types)
272
- for i_progress in range(int(repeat_count)):
273
- if self.stop_pending_event.is_set():
274
- logger.info(f'Stopping execution during {run_name}: {i_progress + 1}/{int(repeat_count)}')
275
- break
276
- logger.info(f'Executing {run_name} experiment: {i_progress + 1}/{int(repeat_count)}')
277
- progress = (i_progress + 1) * 100 / int(repeat_count) - 0.1
278
- self._emit_progress(socketio, progress)
279
- if bo_args:
280
- try:
281
- parameters, trial_index = ax_client.get_next_trial()
282
- logger.info(f'Output value: {parameters}')
283
- # fname = f"{run_name}_script"
284
- # function = self.globals_dict[fname]
285
- output = self.exec_steps(script, "script", logger, socketio, run_id, i_progress, **parameters)
286
-
287
- _output = {key: value for key, value in output.items() if key in return_list}
288
- ax_client.complete_trial(trial_index=trial_index, raw_data=_output)
289
- output.update(parameters)
290
- except Exception as e:
291
- logger.info(f'Optimization error: {e}')
292
- break
293
- else:
294
- # fname = f"{run_name}_script"
295
- # function = self.globals_dict[fname]
296
- output = self.exec_steps(script, "script", logger, socketio, run_id, i_progress)
297
-
298
- if output:
299
- output_list.append(output)
300
- logger.info(f'Output value: {output}')
301
- return output_list
302
-
303
- @staticmethod
304
- def _save_results(run_name, arg_type, return_list, output_list, logger, output_path):
305
- args = list(arg_type.keys()) if arg_type else []
306
- args.extend(return_list)
307
- filename = run_name + "_" + datetime.now().strftime("%Y-%m-%d %H-%M") + ".csv"
308
- file_path = os.path.join(output_path, filename)
309
- with open(file_path, "w", newline='') as file:
310
- writer = csv.DictWriter(file, fieldnames=args)
311
- writer.writeheader()
312
- writer.writerows(output_list)
313
- logger.info(f'Results saved to {file_path}')
314
- return filename
315
-
316
- @staticmethod
317
- def _emit_progress(socketio, progress):
318
- socketio.emit('progress', {'progress': progress})
319
-
320
- def safe_sleep(self, duration: float):
321
- interval = 1 # check every 1 second
322
- end_time = time.time() + duration
323
- while time.time() < end_time:
324
- if self.stop_current_event.is_set():
325
- return # Exit early if stop is requested
326
- time.sleep(min(interval, end_time - time.time()))
327
-
328
- def get_status(self):
329
- """Returns current status of the script runner."""
330
- with self.current_app.app_context():
331
- return {
332
- "is_running": self.lock.locked(),
333
- "paused": self.paused,
334
- "stop_pending": self.stop_pending_event.is_set(),
335
- "stop_current": self.stop_current_event.is_set(),
336
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/task_runner.py DELETED
@@ -1,81 +0,0 @@
1
- import threading
2
- import time
3
- from datetime import datetime
4
-
5
- from ivoryos.utils.db_models import db, SingleStep
6
- from ivoryos.utils.global_config import GlobalConfig
7
-
8
- global_config = GlobalConfig()
9
- global deck
10
- deck = None
11
-
12
-
13
- class TaskRunner:
14
- def __init__(self, globals_dict=None):
15
- self.retry = False
16
- if globals_dict is None:
17
- globals_dict = globals()
18
- self.globals_dict = globals_dict
19
- self.lock = global_config.runner_lock
20
-
21
-
22
- def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
23
- global deck
24
- if deck is None:
25
- deck = global_config.deck
26
-
27
- # Try to acquire lock without blocking
28
- if not self.lock.acquire(blocking=False):
29
- current_status = global_config.runner_status
30
- current_status["status"] = "busy"
31
- return current_status
32
-
33
-
34
- if wait:
35
- output = self._run_single_step(component, method, kwargs, current_app)
36
- else:
37
- print("running with thread")
38
- thread = threading.Thread(
39
- target=self._run_single_step, args=(component, method, kwargs, current_app)
40
- )
41
- thread.start()
42
- time.sleep(0.1)
43
- output = {"status": "task started", "task_id": global_config.runner_status.get("id")}
44
-
45
- return output
46
-
47
- def _get_executable(self, component, deck, method):
48
- if component.startswith("deck."):
49
- component = component.split(".")[1]
50
- instrument = getattr(deck, component)
51
- else:
52
- temp_connections = global_config.defined_variables
53
- instrument = temp_connections.get(component)
54
- function_executable = getattr(instrument, method)
55
- return function_executable
56
-
57
- def _run_single_step(self, component, method, kwargs, current_app=None):
58
- try:
59
- function_executable = self._get_executable(component, deck, method)
60
- method_name = f"{function_executable.__self__.__class__.__name__}.{function_executable.__name__}"
61
- except Exception as e:
62
- self.lock.release()
63
- return {"status": "error", "msg": e.__str__()}
64
-
65
- # with self.lock:
66
- with current_app.app_context():
67
- step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=False, start_time=datetime.now())
68
- db.session.add(step)
69
- db.session.commit()
70
- global_config.runner_status = {"id":step.id, "type": "task"}
71
- try:
72
- output = function_executable(**kwargs)
73
- step.output = output
74
- step.end_time = datetime.now()
75
- except Exception as e:
76
- step.run_error = e.__str__()
77
- step.end_time = datetime.now()
78
- finally:
79
- db.session.commit()
80
- self.lock.release()
81
- return output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/utils/utils.py DELETED
@@ -1,422 +0,0 @@
1
- import ast
2
- import importlib
3
- import inspect
4
- import logging
5
- import os
6
- import pickle
7
- import socket
8
- import subprocess
9
- import sys
10
- from collections import Counter
11
-
12
- import flask
13
- from flask import session
14
- from flask_socketio import SocketIO
15
-
16
- from ivoryos.utils.db_models import Script
17
-
18
-
19
- def get_script_file():
20
- """Get script from Flask session and returns the script"""
21
- session_script = session.get("scripts")
22
- if session_script:
23
- s = Script()
24
- s.__dict__.update(**session_script)
25
- return s
26
- else:
27
- return Script(author=session.get('user'))
28
-
29
-
30
- def post_script_file(script, is_dict=False):
31
- """
32
- Post script to Flask. Script will be converted to a dict if it is a Script object
33
- :param script: Script to post
34
- :param is_dict: if the script is a dictionary,
35
- """
36
- if is_dict:
37
- session['scripts'] = script
38
- else:
39
- session['scripts'] = script.as_dict()
40
-
41
-
42
- def create_gui_dir(parent_path):
43
- """
44
- Creates folders for ivoryos data
45
- """
46
- os.makedirs(parent_path, exist_ok=True)
47
- for path in ["config_csv", "scripts", "results", "pseudo_deck"]:
48
- os.makedirs(os.path.join(parent_path, path), exist_ok=True)
49
-
50
-
51
- def save_to_history(filepath, history_path):
52
- """
53
- For manual deck connection only
54
- save deck file path that successfully connected to ivoryos to a history file
55
- """
56
- connections = []
57
- try:
58
- with open(history_path, 'r') as file:
59
- lines = file.read()
60
- connections = lines.split('\n')
61
- except FileNotFoundError:
62
- pass
63
- if filepath not in connections:
64
- with open(history_path, 'a') as file:
65
- file.writelines(f"{filepath}\n")
66
-
67
-
68
- def import_history(history_path):
69
- """
70
- For manual deck connection only
71
- load deck connection history from history file
72
- """
73
- connections = []
74
- try:
75
- with open(history_path, 'r') as file:
76
- lines = file.read()
77
- connections = lines.split('\n')
78
- except FileNotFoundError:
79
- pass
80
- connections = [i for i in connections if not i == '']
81
- return connections
82
-
83
-
84
- def available_pseudo_deck(path):
85
- """
86
- load pseudo deck (snapshot) from connection history
87
- """
88
- return os.listdir(path)
89
-
90
-
91
- def _inspect_class(class_object=None, debug=False):
92
- """
93
- inspect class object: inspect function signature if not name.startswith("_")
94
- :param class_object: class object
95
- :param debug: debug mode will inspect function.startswith("_")
96
- :return: function: Dict[str, Dict[str, Union[Signature, str, None]]]
97
- """
98
- functions = {}
99
- under_score = "_"
100
- if debug:
101
- under_score = "__"
102
- for function, method in inspect.getmembers(type(class_object), predicate=callable):
103
- if not function.startswith(under_score) and not function.isupper():
104
- try:
105
- annotation = inspect.signature(method)
106
- docstring = inspect.getdoc(method)
107
- functions[function] = dict(signature=annotation, docstring=docstring)
108
-
109
- except Exception:
110
- pass
111
- return functions
112
-
113
-
114
- def _get_type_from_parameters(arg, parameters):
115
- """get argument types from inspection"""
116
- arg_type = ''
117
- if type(parameters) is inspect.Signature:
118
- annotation = parameters.parameters[arg].annotation
119
- elif type(parameters) is dict:
120
- annotation = parameters[arg]
121
- if annotation is not inspect._empty:
122
- # print(p[arg].annotation)
123
- if annotation.__module__ == 'typing':
124
-
125
- if hasattr(annotation, '__origin__'):
126
- origin = annotation.__origin__
127
- if hasattr(origin, '_name') and origin._name in ["Optional", "Union"]:
128
- arg_type = [i.__name__ for i in annotation.__args__]
129
- elif hasattr(origin, '__name__'):
130
- arg_type = origin.__name__
131
- # todo other types
132
- elif annotation.__module__ == 'types':
133
- arg_type = [i.__name__ for i in annotation.__args__]
134
-
135
- else:
136
- arg_type = annotation.__name__
137
- return arg_type
138
-
139
-
140
- def _convert_by_str(args, arg_types):
141
- """
142
- Converts a value to type through eval(f'{type}("{args}")')
143
- """
144
- if type(arg_types) is not list:
145
- arg_types = [arg_types]
146
- for arg_type in arg_types:
147
- if not arg_type == "any":
148
- try:
149
- args = eval(f'{arg_type}("{args}")') if type(args) is str else eval(f'{arg_type}({args})')
150
- return args
151
- except Exception:
152
- raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
153
-
154
-
155
- def _convert_by_class(args, arg_types):
156
- """
157
- Converts a value to type through type(arg)
158
- """
159
- if arg_types.__module__ == 'builtins':
160
- args = arg_types(args)
161
- return args
162
- elif arg_types.__module__ == "typing":
163
- for i in arg_types.__args__: # for typing.Union
164
- try:
165
- args = i(args)
166
- return args
167
- except Exception:
168
- pass
169
- raise TypeError("Input type error.")
170
- # else:
171
- # args = globals()[args]
172
- return args
173
-
174
-
175
- def convert_config_type(args, arg_types, is_class: bool = False):
176
- """
177
- Converts an argument from str to an arg type
178
- """
179
- if args:
180
- for arg in args:
181
- if arg not in arg_types.keys():
182
- raise ValueError("config file format not supported.")
183
- if args[arg] == '' or args[arg] == "None":
184
- args[arg] = None
185
- # elif args[arg] == "True" or args[arg] == "False":
186
- # args[arg] = bool_dict[args[arg]]
187
- else:
188
- arg_type = arg_types[arg]
189
- try:
190
- args[arg] = ast.literal_eval(args[arg])
191
- except ValueError:
192
- pass
193
- if type(args[arg]) is not arg_type and not type(args[arg]).__name__ == arg_type:
194
- if is_class:
195
- # if arg_type.__module__ == 'builtins':
196
- args[arg] = _convert_by_class(args[arg], arg_type)
197
- else:
198
- args[arg] = _convert_by_str(args[arg], arg_type)
199
- return args
200
-
201
-
202
- def import_module_by_filepath(filepath: str, name: str):
203
- """
204
- Import module by file path
205
- :param filepath: full path of module
206
- :param name: module's name
207
- """
208
- spec = importlib.util.spec_from_file_location(name, filepath)
209
- module = importlib.util.module_from_spec(spec)
210
- spec.loader.exec_module(module)
211
- return module
212
-
213
-
214
- class SocketIOHandler(logging.Handler):
215
- def __init__(self, socketio: SocketIO):
216
- super().__init__()
217
- self.formatter = logging.Formatter('%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
218
- self.socketio = socketio
219
-
220
- def emit(self, record):
221
- message = self.format(record)
222
- # session["last_log"] = message
223
- self.socketio.emit('log', {'message': message})
224
-
225
-
226
- def start_logger(socketio: SocketIO, logger_name: str, log_filename: str = None):
227
- """
228
- stream logger to web through web socketIO
229
- """
230
- # logging.basicConfig( format='%(asctime)s - %(message)s')
231
- formatter = logging.Formatter(fmt='%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
232
- logger = logging.getLogger(logger_name)
233
- logger.setLevel(logging.INFO)
234
- file_handler = logging.FileHandler(filename=log_filename, )
235
- file_handler.setFormatter(formatter)
236
- logger.addHandler(file_handler)
237
- # console_logger = logging.StreamHandler() # stream to console
238
- # logger.addHandler(console_logger)
239
- socketio_handler = SocketIOHandler(socketio)
240
- logger.addHandler(socketio_handler)
241
- return logger
242
-
243
-
244
- def get_arg_type(args, parameters):
245
- """get argument type from signature"""
246
- arg_types = {}
247
- # print(args, parameters)
248
- parameters = parameters.get("signature")
249
- if args:
250
- for arg in args:
251
- arg_types[arg] = _get_type_from_parameters(arg, parameters)
252
- return arg_types
253
-
254
-
255
- def install_and_import(package, package_name=None):
256
- """
257
- Install the package and import it
258
- :param package: package to import and install
259
- :param package_name: pip install package name if different from package
260
- """
261
- try:
262
- # Check if the package is already installed
263
- importlib.import_module(package)
264
- # print(f"{package} is already installed.")
265
- except ImportError:
266
- # If not installed, install it
267
- # print(f"{package} is not installed. Installing now...")
268
- subprocess.check_call([sys.executable, "-m", "pip", "install", package_name or package])
269
- # print(f"{package} has been installed successfully.")
270
-
271
-
272
- def web_config_entry_wrapper(data: dict, config_type: list):
273
- """
274
- Wrap the data dictionary from web config entries during execution configuration
275
- :param data: data dictionary
276
- :param config_type: data entry types ["str", "int", "float", "bool"]
277
- """
278
- rows = {} # Dictionary to hold webui_data organized by rows
279
-
280
- # Organize webui_data by rows
281
- for key, value in data.items():
282
- if value: # Only process non-empty values
283
- # Extract the field name and row index
284
- field_name, row_index = key.split('[')
285
- row_index = int(row_index.rstrip(']'))
286
-
287
- # If row not in rows, create a new dictionary for that row
288
- if row_index not in rows:
289
- rows[row_index] = {}
290
-
291
- # Add or update the field value in the specific row's dictionary
292
- rows[row_index][field_name] = value
293
-
294
- # Filter out any empty rows and create a list of dictionaries
295
- filtered_rows = [row for row in rows.values() if len(row) == len(config_type)]
296
-
297
- return filtered_rows
298
-
299
-
300
- def create_deck_snapshot(deck, save: bool = False, output_path: str = '', exclude_names: list[str] = []):
301
- """
302
- Create a deck snapshot of the given script
303
- :param deck: python module name to create the deck snapshot from e.g. __main__
304
- :param save: save the deck snapshot into pickle file
305
- :param output_path: path to save the pickle file
306
- :param exclude_names: module names to exclude from deck snapshot
307
- """
308
- exclude_classes = (flask.Blueprint, logging.Logger)
309
-
310
- deck_snapshot = {}
311
- included = {}
312
- excluded = {}
313
- failed = {}
314
-
315
- for name, val in vars(deck).items():
316
- qualified_name = f"deck.{name}"
317
-
318
- # Exclusion checks
319
- if (
320
- type(val).__module__ == 'builtins'
321
- or name[0].isupper()
322
- or name.startswith("_")
323
- or isinstance(val, exclude_classes)
324
- or name in exclude_names
325
- ):
326
- excluded[qualified_name] = type(val).__name__
327
- continue
328
-
329
- try:
330
- deck_snapshot[qualified_name] = _inspect_class(val)
331
- included[qualified_name] = type(val).__name__
332
- except Exception as e:
333
- failed[qualified_name] = str(e)
334
-
335
- # Final result
336
- deck_summary = {
337
- "included": included,
338
- # "excluded": excluded,
339
- "failed": failed
340
- }
341
-
342
- def print_deck_snapshot(deck_summary):
343
- def print_section(title, items):
344
- print(f"\n=== {title} ({len(items)}) ===")
345
- if not items:
346
- return
347
- for name, class_type in items.items():
348
- print(f" {name}: {class_type}")
349
-
350
- print_section("✅ INCLUDED", deck_summary["included"])
351
- print_section("❌ FAILED", deck_summary["failed"])
352
- print("\n")
353
-
354
- print_deck_snapshot(deck_summary)
355
-
356
- if deck_snapshot and save:
357
- # pseudo_deck = parse_dict
358
- parse_dict = deck_snapshot.copy()
359
- parse_dict["deck_name"] = os.path.splitext(os.path.basename(deck.__file__))[
360
- 0] if deck.__name__ == "__main__" else deck.__name__
361
- with open(os.path.join(output_path, f"{parse_dict['deck_name']}.pkl"), 'wb') as file:
362
- pickle.dump(parse_dict, file)
363
- return deck_snapshot
364
-
365
-
366
- def load_deck(pkl_name: str):
367
- """
368
- Loads a pickled deck snapshot from disk on offline mode
369
- :param pkl_name: name of the pickle file
370
- """
371
- if not pkl_name:
372
- return None
373
- try:
374
- with open(pkl_name, 'rb') as f:
375
- pseudo_deck = pickle.load(f)
376
- return pseudo_deck
377
- except FileNotFoundError:
378
- return None
379
-
380
-
381
- def check_config_duplicate(config):
382
- """
383
- Checks if the config entry has any duplicate
384
- :param config: [{"arg": 1}, {"arg": 1}, {"arg": 1}]
385
- :return: [True, False]
386
- """
387
- hashable_data = [tuple(sorted(d.items())) for d in config]
388
- return any(count > 1 for count in Counter(hashable_data).values())
389
-
390
-
391
- def get_method_from_workflow(function_string, func_name="workflow"):
392
- """Creates a function from a string and assigns it a new name."""
393
-
394
- namespace = {}
395
- exec(function_string, globals(), namespace) # Execute the string in a safe namespace
396
- # func_name = next(iter(namespace))
397
- # Get the function name dynamically
398
- return namespace[func_name]
399
-
400
- # def load_workflows(script):
401
- #
402
- # class RegisteredWorkflows:
403
- # pass
404
- # deck_name = script.deck
405
- # workflows = Script.query.filter(Script.deck == deck_name, Script.name != script.name, Script.registered==True).all()
406
- # for workflow in workflows:
407
- # compiled_strs = workflow.compile().get('script', "")
408
- # method = get_method_from_workflow(compiled_strs, func_name=workflow.name)
409
- # setattr(RegisteredWorkflows, workflow.name, staticmethod(method))
410
- # global_config.registered_workflows = RegisteredWorkflows()
411
-
412
-
413
- def get_local_ip():
414
- try:
415
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
416
- s.connect(('10.255.255.255', 1)) # Dummy address to get interface IP
417
- ip = s.getsockname()[0]
418
- except Exception:
419
- ip = '127.0.0.1'
420
- finally:
421
- s.close()
422
- return ip
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ivoryos/version.py DELETED
@@ -1 +0,0 @@
1
- __version__ = "1.0.7"
 
 
requirements.txt CHANGED
@@ -1,11 +1 @@
1
- # Flask and SQL
2
- bcrypt
3
- Flask-Login
4
- Flask-Session
5
- Flask-SocketIO
6
- Flask-SQLAlchemy
7
- SQLAlchemy-Utils~=0.41
8
- wtforms~=3.2.1
9
- flask-wtf
10
- eventlet
11
- python-dotenv
 
1
+ ivoryos>=1.0.7