ivoryos 0.1.12__py3-none-any.whl → 0.1.19__py3-none-any.whl

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

Potentially problematic release.


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

ivoryos/__init__.py CHANGED
@@ -1,8 +1,10 @@
1
+ import importlib
2
+ import inspect
1
3
  import os
2
4
  import sys
3
5
  from typing import Union
4
6
 
5
- from flask import Flask, redirect, url_for
7
+ from flask import Flask, redirect, url_for, Blueprint, g
6
8
 
7
9
  from ivoryos.config import Config, get_config
8
10
  from ivoryos.routes.auth.auth import auth, login_manager
@@ -10,22 +12,18 @@ from ivoryos.routes.control.control import control
10
12
  from ivoryos.routes.database.database import database
11
13
  from ivoryos.routes.design.design import design, socketio
12
14
  from ivoryos.routes.main.main import main
15
+ # from ivoryos.routes.monitor.monitor import monitor
13
16
  from ivoryos.utils import utils
14
17
  from ivoryos.utils.db_models import db
15
18
  from ivoryos.utils.global_config import GlobalConfig
16
19
  from ivoryos.utils.script_runner import ScriptRunner
17
20
  from ivoryos.version import __version__ as ivoryos_version
18
-
21
+ from importlib.metadata import entry_points
19
22
  global_config = GlobalConfig()
20
23
 
21
-
22
24
  url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
23
25
  app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
24
- app.register_blueprint(main, url_prefix=url_prefix)
25
- app.register_blueprint(auth, url_prefix=url_prefix)
26
- app.register_blueprint(design, url_prefix=url_prefix)
27
- app.register_blueprint(database, url_prefix=url_prefix)
28
- app.register_blueprint(control, url_prefix=url_prefix)
26
+
29
27
 
30
28
  def create_app(config_class=None):
31
29
  # url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
@@ -55,12 +53,9 @@ def create_app(config_class=None):
55
53
  Called before
56
54
 
57
55
  """
58
- from flask import g
59
56
  g.logger = logger
60
57
  g.socketio = socketio
61
58
 
62
-
63
-
64
59
  @app.route('/')
65
60
  def redirect_to_prefix():
66
61
  return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
@@ -72,6 +67,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
72
67
  config: Config = None,
73
68
  logger: Union[str, list] = None,
74
69
  logger_output_name: str = None,
70
+ enable_design=True
75
71
  ):
76
72
  """
77
73
  Start ivoryOS app server.
@@ -85,22 +81,43 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
85
81
  :param config: config class, defaults to None
86
82
  :param logger: logger name of list of logger names, defaults to None
87
83
  :param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
88
-
84
+ :param enable_design: enable design canvas, database and workflow execution
85
+ :param stream_address:
89
86
  """
90
87
  app = create_app(config_class=config or get_config()) # Create app instance using factory function
91
88
 
89
+ app.register_blueprint(main, url_prefix=url_prefix)
90
+ app.register_blueprint(auth, url_prefix=url_prefix)
91
+ app.register_blueprint(control, url_prefix=url_prefix)
92
+
93
+ if enable_design:
94
+ app.register_blueprint(design, url_prefix=url_prefix)
95
+ app.register_blueprint(database, url_prefix=url_prefix)
96
+
97
+ plugins = load_plugins(app, socketio)
98
+
99
+ def inject_nav_config():
100
+ """Make NAV_CONFIG available globally to all templates."""
101
+ return dict(
102
+ enable_design=enable_design,
103
+ plugins=plugins,
104
+ )
105
+
106
+ app.context_processor(inject_nav_config)
92
107
  port = port or int(os.environ.get("PORT", 8000))
93
108
  debug = debug if debug is not None else app.config.get('DEBUG', True)
94
109
 
95
110
  app.config["LOGGERS"] = logger
96
- app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
111
+ app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
97
112
  logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
98
113
 
99
114
  if module:
100
115
  app.config["MODULE"] = module
101
116
  app.config["OFF_LINE"] = False
102
117
  global_config.deck = sys.modules[module]
103
- global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck, output_path=app.config["DUMMY_DECK"], save=True)
118
+ # global_config.heinsight = HeinsightAPI("http://127.0.0.1:8080")
119
+ global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
120
+ output_path=app.config["DUMMY_DECK"], save=True)
104
121
  # global_config.runner = ScriptRunner(globals())
105
122
  else:
106
123
  app.config["OFF_LINE"] = True
@@ -120,3 +137,22 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
120
137
  for log in logger:
121
138
  utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
122
139
  socketio.run(app, host=host, port=port, debug=debug, use_reloader=False, allow_unsafe_werkzeug=True)
140
+ # return app
141
+
142
+
143
+ def load_plugins(app, socketio):
144
+ """
145
+ Dynamically load installed plugins and attach Flask-SocketIO.
146
+ """
147
+ plugin_names = []
148
+ for entry_point in entry_points().get("ivoryos.plugins", []):
149
+ plugin = entry_point.load()
150
+
151
+ # If the plugin has an `init_socketio()` function, pass socketio
152
+ if hasattr(plugin, 'init_socketio'):
153
+ plugin.init_socketio(socketio)
154
+
155
+ plugin_names.append(entry_point.name)
156
+ app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
157
+
158
+ return plugin_names
@@ -16,6 +16,7 @@
16
16
  {% if function not in hidden_instrument %}
17
17
  <div class="card" id="{{function}}">
18
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>
19
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>
20
21
  <div class="form-control" style="border: none">
21
22
  <form role="form" method='POST' name="{{function}}" id="{{function}}">
@@ -38,7 +39,9 @@
38
39
  </div>
39
40
  <div class="input-group mb-3">
40
41
  <button type="submit" name="{{ function }}" id="{{ function }}" class="form-control" style="background-color: #a5cece;">{{format_name(function)}} </button>
42
+
41
43
  </div>
44
+
42
45
  </form>
43
46
  </div>
44
47
  </div>
@@ -12,7 +12,8 @@ from werkzeug.utils import secure_filename
12
12
 
13
13
  from ivoryos.utils import utils
14
14
  from ivoryos.utils.global_config import GlobalConfig
15
- from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo
15
+ from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
16
+ create_form_from_action
16
17
  from ivoryos.utils.db_models import Script
17
18
  from ivoryos.utils.script_runner import ScriptRunner
18
19
 
@@ -23,10 +24,22 @@ global_config = GlobalConfig()
23
24
  runner = ScriptRunner()
24
25
 
25
26
 
26
- @socketio.on('abort_action')
27
- def handle_abort_action():
27
+ @socketio.on('abort_pending')
28
+ def handle_abort_pending():
29
+ runner.abort_pending()
30
+ socketio.emit('log', {'message': "aborted pending iterations"})
31
+
32
+
33
+ @socketio.on('abort_current')
34
+ def handle_abort_current():
28
35
  runner.stop_execution()
29
- socketio.emit('log', {'message': "aborted pending tasks"})
36
+ socketio.emit('log', {'message': "stopped next task"})
37
+
38
+
39
+ @socketio.on('pause')
40
+ def handle_pause():
41
+ msg = runner.toggle_pause()
42
+ socketio.emit('log', {'message': msg})
30
43
 
31
44
 
32
45
  @socketio.on('connect')
@@ -99,25 +112,29 @@ def experiment_builder(instrument=None):
99
112
 
100
113
  deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
101
114
 
102
- functions = []
115
+ functions = {}
103
116
  if deck:
104
117
  deck_variables = global_config.deck_snapshot.keys()
105
118
  else:
106
119
  deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
107
120
  deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
108
- if instrument:
121
+ edit_action_info = session.get("edit_action")
122
+ if edit_action_info:
123
+ forms = create_form_from_action(edit_action_info, script=script)
124
+ elif instrument:
109
125
  if instrument in ['if', 'while', 'variable', 'wait', 'repeat']:
110
- forms = create_builtin_form(instrument, autofill=autofill, script=script)
126
+ forms = create_builtin_form(instrument, script=script)
111
127
  else:
112
128
  if deck:
113
- function_metadata = global_config.deck_snapshot.get(instrument, {})
129
+ functions = global_config.deck_snapshot.get(instrument, {})
114
130
  elif pseudo_deck:
115
- function_metadata = pseudo_deck.get(instrument, {})
116
- functions = {key: data.get('signature', {}) for key, data in function_metadata.items()}
131
+ functions = pseudo_deck.get(instrument, {})
132
+ # print(function_metadata)
133
+ # functions = {key: data.get('signature', {}) for key, data in function_metadata.items()}
117
134
  forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
118
135
  if request.method == 'POST' and "hidden_name" in request.form:
119
- all_kwargs = request.form.copy()
120
- method_name = all_kwargs.pop("hidden_name", None)
136
+ # all_kwargs = request.form.copy()
137
+ method_name = request.form.get("hidden_name", None)
121
138
  # if method_name is not None:
122
139
  form = forms.get(method_name)
123
140
  kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
@@ -125,29 +142,15 @@ def experiment_builder(instrument=None):
125
142
  if form and form.validate_on_submit():
126
143
  function_name = kwargs.pop("hidden_name")
127
144
  save_data = kwargs.pop('return', '')
128
- variable_kwargs = {}
129
- variable_kwargs_types = {}
130
145
 
131
- try:
132
- variable_kwargs, variable_kwargs_types = utils.find_variable_in_script(script, kwargs)
133
-
134
- for name in variable_kwargs.keys():
135
- del kwargs[name]
136
- primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
137
-
138
- except:
139
- primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
140
-
141
- kwargs.update(variable_kwargs)
142
- arg_types = {}
143
- arg_types.update(variable_kwargs_types)
144
- arg_types.update(primitive_arg_types)
145
- all_kwargs.update(variable_kwargs)
146
+ primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
146
147
 
148
+ script.eval_list(kwargs, primitive_arg_types)
149
+ kwargs = script.validate_variables(kwargs)
147
150
  action = {"instrument": instrument, "action": function_name,
148
- "args": {name: arg for (name, arg) in kwargs.items()},
151
+ "args": kwargs,
149
152
  "return": save_data,
150
- 'arg_types': arg_types}
153
+ 'arg_types': primitive_arg_types}
151
154
  script.add_action(action=action)
152
155
  else:
153
156
  flash(form.errors)
@@ -155,9 +158,13 @@ def experiment_builder(instrument=None):
155
158
  elif request.method == 'POST' and "builtin_name" in request.form:
156
159
  kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
157
160
  if forms.validate_on_submit():
161
+ # print(kwargs)
158
162
  logic_type = kwargs.pop('builtin_name')
159
163
  if 'variable' in kwargs:
160
- script.add_variable(**kwargs)
164
+ try:
165
+ script.add_variable(**kwargs)
166
+ except ValueError:
167
+ flash("Invalid variable type")
161
168
  else:
162
169
  script.add_logic_action(logic_type=logic_type, **kwargs)
163
170
  else:
@@ -168,12 +175,13 @@ def experiment_builder(instrument=None):
168
175
  autofill = not autofill
169
176
  forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
170
177
  session['autofill'] = autofill
178
+
171
179
  utils.post_script_file(script)
172
- design_buttons = [create_action_button(i) for i in script.currently_editing_script]
180
+ design_buttons = create_action_button(script)
173
181
  return render_template('experiment_builder.html', off_line=off_line, instrument=instrument, history=deck_list,
174
182
  script=script, defined_variables=deck_variables,
175
183
  local_variables=global_config.defined_variables,
176
- functions=functions, forms=forms, buttons=design_buttons, format_name=format_name,
184
+ forms=forms, buttons=design_buttons, format_name=format_name,
177
185
  use_llm=enable_llm)
178
186
 
179
187
 
@@ -250,10 +258,14 @@ def experiment_run():
250
258
  # module = current_app.config.get('MODULE', '')
251
259
  # deck = sys.modules[module] if module else None
252
260
  # script.deck = os.path.splitext(os.path.basename(deck.__file__))[0]
253
- design_buttons = {stype: [create_action_button(i) for i in script.get_script(stype)] for stype in script.stypes}
261
+ design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
254
262
  config_preview = []
255
263
  config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
256
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
264
+ try:
265
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
266
+ except ValueError as e:
267
+ flash(e.__str__())
268
+ return redirect(url_for("design.experiment_builder"))
257
269
  # print(exec_string)
258
270
  config_file = request.args.get("filename")
259
271
  config = []
@@ -266,9 +278,10 @@ def experiment_run():
266
278
  config_preview = config[1:6]
267
279
  arg_type = config.pop(0) # first entry is types
268
280
  try:
269
- exec(exec_string)
281
+ for key, func_str in exec_string.items():
282
+ exec(func_str)
270
283
  except Exception:
271
- flash("Please check syntax!!")
284
+ flash(f"Please check {key} syntax!!")
272
285
  return redirect(url_for("design.experiment_builder"))
273
286
  # runner.globals_dict.update(globals())
274
287
  run_name = script.name if script.name else "untitled"
@@ -313,7 +326,8 @@ def experiment_run():
313
326
  return render_template('experiment_run.html', script=script.script_dict, filename=filename, dot_py=exec_string,
314
327
  return_list=return_list, config_list=config_list, config_file_list=config_file_list,
315
328
  config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
316
- no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons, history=deck_list)
329
+ no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons,
330
+ history=deck_list)
317
331
 
318
332
 
319
333
  @design.route("/toggle_script_type/<stype>")
@@ -492,14 +506,20 @@ def edit_action(uuid: str):
492
506
  script = utils.get_script_file()
493
507
  action = script.find_by_uuid(uuid)
494
508
  session['edit_action'] = action
495
- if request.method == "POST":
509
+
510
+ if request.method == "POST" and action is not None:
511
+ forms = create_form_from_action(action, script=script)
496
512
  if "back" not in request.form:
497
- args = request.form.to_dict()
498
- save_as = args.pop('return', '')
499
- try:
500
- script.update_by_uuid(uuid=uuid, args=args, output=save_as)
501
- except Exception as e:
502
- flash(e.__str__())
513
+ kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
514
+ # print(kwargs)
515
+ if forms and forms.validate_on_submit():
516
+ save_as = kwargs.pop('return', '')
517
+ kwargs = script.validate_variables(kwargs)
518
+ # try:
519
+ script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as)
520
+ # except Exception as e:
521
+ else:
522
+ flash(forms.errors)
503
523
  session.pop('edit_action')
504
524
  return redirect(url_for('design.experiment_builder'))
505
525
 
@@ -541,4 +561,4 @@ def duplicate_action(id: int):
541
561
  script = utils.get_script_file()
542
562
  script.duplicate_action(id)
543
563
  utils.post_script_file(script)
544
- return redirect(back)
564
+ return redirect(back)
@@ -30,6 +30,7 @@
30
30
 
31
31
  {# edit action #}
32
32
  {% if session["edit_action"] %}
33
+ {# {{ session["edit_action"] }}#}
33
34
  {% with action = session["edit_action"] %}
34
35
  <h5> {{ format_name(action['action']) }} </h5>
35
36
  <form role="form" method='POST' name="{{instrument}}" action="{{ url_for('design.edit_action', uuid=session["edit_action"]['uuid']) }}">
@@ -41,16 +42,26 @@
41
42
  <input class="form-control" type="text" id="arg" name="arg" placeholder="{{ action['arg_types']}}" value="{{ action['args'] }}" aria-labelledby="variableHelpBlock">
42
43
  </div>
43
44
  {% else %}
44
- {% for arg in action['args'] %}
45
- <div class="input-group mb-3">
46
- <label class="input-group-text">{{ format_name(arg) }}</label>
47
- <input class="form-control" type="text" id="{{ arg }}" name="{{ arg }}" placeholder="{{ action['arg_types'][arg] }}" value="{{ action['args'][arg] }}" aria-labelledby="variableHelpBlock">
48
- </div>
45
+ {# {% for arg in action['args'] %}#}
46
+ {# <div class="input-group mb-3">#}
47
+ {# <label class="input-group-text">{{ format_name(arg) }}</label>#}
48
+ {# <input class="form-control" type="text" id="{{ arg }}" name="{{ arg }}" placeholder="{{ action['arg_types'][arg] }}" value="{{ action['args'][arg] }}" aria-labelledby="variableHelpBlock">#}
49
+ {# </div>#}
50
+ {# {% endfor %}#}
51
+ {# <div class="input-group mb-3">#}
52
+ {# <label class="input-group-text">Save Output?</label>#}
53
+ {# <input class="form-control" type="text" id="return" name="return" value="{{ action['return'] }}" aria-labelledby="variableHelpBlock">#}
54
+ {# </div>#}
55
+ {{ forms.hidden_tag() }}
56
+ {% for field in forms %}
57
+ {% if field.type not in ['CSRFTokenField'] %}
58
+ <div class="input-group mb-3">
59
+ <label class="input-group-text">{{ field.label.text }}</label>
60
+ {{ field(class="form-control") }}
61
+ <div class="form-text">{{ field.description }} </div>
62
+ </div>
63
+ {% endif %}
49
64
  {% endfor %}
50
- <div class="input-group mb-3">
51
- <label class="input-group-text">Save Output?</label>
52
- <input class="form-control" type="text" id="return" name="return" value="{{ action['return'] }}" aria-labelledby="variableHelpBlock">
53
- </div>
54
65
  {% endif %}
55
66
  </div>
56
67
  {% endif %}
@@ -139,12 +150,12 @@
139
150
  {{ format_name(name) }}
140
151
  </button>
141
152
  </h2>
153
+
142
154
  <div id="{{name}}" class="accordion-collapse collapse" data-bs-parent="#accordionActions">
143
155
  <div class="accordion-body">
144
156
  <form role="form" method='POST' name="add" id="add">
145
157
  <div class="form-group">
146
158
  {{ form.hidden_tag() }}
147
- {# {{ form.hidden_name() }}#}
148
159
  {% for field in form %}
149
160
  {% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
150
161
  <div class="input-group mb-3">
@@ -161,6 +172,8 @@
161
172
  {% endfor %}
162
173
  </div>
163
174
  <button type="submit" class="btn btn-dark">Add </button>
175
+ <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>
176
+
164
177
  </form>
165
178
 
166
179
 
@@ -219,10 +219,24 @@
219
219
  </div>
220
220
  <div class="col-lg-6 col-sm-12 logging-panel">
221
221
  <p>
222
- <div class="p d-flex justify-content-between align-items-center">
223
- <h5>Progress:</h5>
224
- <button id="abort" class="btn btn-danger ">Abort Pending Actions</button>
225
- </div>
222
+ <div class="p d-flex justify-content-between align-items-center">
223
+ <h5>Progress:</h5>
224
+ <div class="d-flex gap-2 ms-auto">
225
+ <button id="pause-resume" class="btn btn-info text-white" data-bs-toggle="tooltip" title="Pause execution">
226
+ <i class="bi bi-pause-circle"></i> <!-- Icon for Pause -->
227
+ </button>
228
+ <button id="abort-current" class="btn btn-danger text-white" data-bs-toggle="tooltip" title="Stop execution after current step">
229
+ <i class="bi bi-stop-circle"></i> <!-- Icon for Stop After This Step -->
230
+ </button>
231
+ <button id="abort-pending" class="btn btn-warning text-white" data-bs-toggle="tooltip" title="Stop execution after current iteration">
232
+ <i class="bi bi-hourglass-split"></i> <!-- Icon for Stop After This Iteration -->
233
+ </button>
234
+ </div>
235
+ </div>
236
+ <div class="text-muted mt-2">
237
+ <small><strong>Note:</strong> The current step cannot be paused or stopped until it completes. </small>
238
+ </div>
239
+
226
240
  </p>
227
241
  <div class="progress" role="progressbar" aria-label="Animated striped example" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100">
228
242
  <div id="progress-bar-inner" class="progress-bar progress-bar-striped progress-bar-animated"></div>
@@ -295,7 +309,9 @@
295
309
  </h2>
296
310
  <div id="python" class="accordion-collapse collapse">
297
311
  <div class="accordion-body">
298
- <pre><code class="python" >{{dot_py}}</code></pre>
312
+ {% for stype, script in dot_py.items() %}
313
+ <pre><code class="python" >{{script}}</code></pre>
314
+ {% endfor %}
299
315
  <a href="{{ url_for('design.download', filetype='python') }}">Download <i class="bi bi-download"></i></a>
300
316
  </div>
301
317
  </div>
@@ -9,32 +9,32 @@
9
9
  </h1>
10
10
  <p>Version: {{ version }}</p>
11
11
  <div class="row">
12
-
13
- <div class="col-lg-6 mb-4 d-flex align-items-stretch">
14
- <div class="card rounded shadow-sm flex-fill">
15
- <div class="card-body">
16
- <h5 class="card-title">Browse designs</h5>
17
- <p class="card-text">Browse all workflows saved in the database.</p>
18
- <a href="{{ url_for('database.load_from_database') }}" class="stretched-link"></a>
12
+ {% if enable_design %}
13
+ <div class="col-lg-6 mb-4 d-flex align-items-stretch">
14
+ <div class="card rounded shadow-sm flex-fill">
15
+ <div class="card-body">
16
+ <h5 class="card-title">Browse designs</h5>
17
+ <p class="card-text">Browse all workflows saved in the database.</p>
18
+ <a href="{{ url_for('database.load_from_database') }}" class="stretched-link"></a>
19
+ </div>
19
20
  </div>
20
21
  </div>
21
- </div>
22
- <div class="col-lg-6 mb-4 d-flex align-items-stretch">
23
- <div class="card rounded shadow-sm flex-fill">
24
- <div class="card-body">
25
- <h5 class="card-title">Edit designs</h5>
26
- <p class="card-text">Build your workflow from current deck functions.</p>
27
- <a href="{{ url_for('design.experiment_builder') }}" class="stretched-link"></a>
22
+ <div class="col-lg-6 mb-4 d-flex align-items-stretch">
23
+ <div class="card rounded shadow-sm flex-fill">
24
+ <div class="card-body">
25
+ <h5 class="card-title">Edit designs</h5>
26
+ <p class="card-text">Build your workflow from current deck functions.</p>
27
+ <a href="{{ url_for('design.experiment_builder') }}" class="stretched-link"></a>
28
+ </div>
28
29
  </div>
29
30
  </div>
30
- </div>
31
+ {% endif %}
31
32
  </div>
32
33
 
33
34
  <br><br><br>
34
35
  {% if not off_line %}
35
- {# <h5>Only available in online mode</h5>#}
36
- {# <hr>#}
37
36
  <div class="row">
37
+ {% if enable_design %}
38
38
  <div class="col-lg-6 mb-4 d-flex align-items-stretch">
39
39
  <div class="card rounded shadow-sm flex-fill">
40
40
  <div class="card-body">
@@ -44,6 +44,8 @@
44
44
  </div>
45
45
  </div>
46
46
  </div>
47
+ {% endif %}
48
+
47
49
  <div class="col-lg-6 mb-4 d-flex align-items-stretch">
48
50
  <div class="card rounded shadow-sm flex-fill">
49
51
  <div class="card-body">
@@ -24,11 +24,39 @@ document.addEventListener("DOMContentLoaded", function() {
24
24
  $('#logging-panel').scrollTop($('#logging-panel')[0].scrollHeight);
25
25
  });
26
26
 
27
- document.getElementById('abort').addEventListener('click', function() {
28
- var confirmation = confirm("Are you sure you want to abort pending actions?");
27
+ document.getElementById('abort-pending').addEventListener('click', function() {
28
+ var confirmation = confirm("Are you sure you want to stop after this iteration?");
29
29
  if (confirmation) {
30
- socket.emit('abort_action');
30
+ socket.emit('abort_pending');
31
31
  console.log('Abort action sent to server.');
32
32
  }
33
33
  });
34
+ document.getElementById('abort-current').addEventListener('click', function() {
35
+ var confirmation = confirm("Are you sure you want to stop after this step?");
36
+ if (confirmation) {
37
+ socket.emit('abort_current');
38
+ console.log('Stop action sent to server.');
39
+ }
40
+ });
41
+
42
+ document.getElementById('pause-resume').addEventListener('click', function() {
43
+
44
+ socket.emit('pause');
45
+ console.log('Pause/Resume is toggled.');
46
+ var button = this;
47
+ var icon = button.querySelector("i");
48
+
49
+ // Toggle between Pause and Resume
50
+ if (icon.classList.contains("bi-pause-circle")) {
51
+ icon.classList.remove("bi-pause-circle");
52
+ icon.classList.add("bi-play-circle");
53
+ button.innerHTML = '<i class="bi bi-play-circle"></i>';
54
+ button.setAttribute("title", "Resume execution");
55
+ } else {
56
+ icon.classList.remove("bi-play-circle");
57
+ icon.classList.add("bi-pause-circle");
58
+ button.innerHTML = '<i class="bi bi-pause-circle"></i>';
59
+ button.setAttribute("title", "Pause execution");
60
+ }
61
+ });
34
62
  });
@@ -23,7 +23,7 @@
23
23
  <body>
24
24
  <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
25
25
  <div class= "container">
26
-
26
+ {# {{ module_config }}#}
27
27
  <a class="navbar-brand" href="{{ url_for('main.index') }}">
28
28
  <img src="{{url_for('static', filename='logo.webp')}}" alt="Logo" height="60" class="d-inline-block align-text-bottom">
29
29
  </a>
@@ -36,15 +36,18 @@
36
36
  <li class="nav-item">
37
37
  <a class="nav-link" href="{{ url_for('main.index') }}" aria-current="page">Home</a>
38
38
  </li>
39
- <li class="nav-item">
40
- <a class="nav-link" href="{{ url_for('database.load_from_database') }}" aria-current="page">Library</a>
41
- </li>
42
- <li class="nav-item">
43
- <a class="nav-link" href="{{ url_for('design.experiment_builder') }}">Design</a>
44
- </li>
45
- <li class="nav-item">
46
- <a class="nav-link" href="{{ url_for('design.experiment_run') }}">Compile/Run</a>
47
- </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
+ {% endif %}
50
+
48
51
  <li class="nav-item">
49
52
  <a class="nav-link" href="{{ url_for('control.deck_controllers') }}">Devices</a></li>
50
53
  </li>
@@ -54,6 +57,13 @@
54
57
  <li class="nav-item">
55
58
  <a class="nav-link" href="{{ url_for('main.help_info') }}">About</a>
56
59
  </li>
60
+ {% if plugins %}
61
+ {% for plugin in plugins %}
62
+ <li class="nav-item">
63
+ <a class="nav-link" href="{{ url_for(plugin+'.main') }}">{{ plugin.capitalize() }}</a></li>
64
+ </li>
65
+ {% endfor %}
66
+ {% endif %}
57
67
  </ul>
58
68
  <ul class="navbar-nav ms-auto">
59
69