ivoryos 0.1.10__py3-none-any.whl → 0.1.18__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")
@@ -51,12 +49,13 @@ def create_app(config_class=None):
51
49
 
52
50
  @app.before_request
53
51
  def before_request():
54
- from flask import g
52
+ """
53
+ Called before
54
+
55
+ """
55
56
  g.logger = logger
56
57
  g.socketio = socketio
57
58
 
58
-
59
-
60
59
  @app.route('/')
61
60
  def redirect_to_prefix():
62
61
  return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
@@ -68,6 +67,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
68
67
  config: Config = None,
69
68
  logger: Union[str, list] = None,
70
69
  logger_output_name: str = None,
70
+ enable_design=True
71
71
  ):
72
72
  """
73
73
  Start ivoryOS app server.
@@ -81,22 +81,43 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
81
81
  :param config: config class, defaults to None
82
82
  :param logger: logger name of list of logger names, defaults to None
83
83
  :param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
84
-
84
+ :param enable_design: enable design canvas, database and workflow execution
85
+ :param stream_address:
85
86
  """
86
87
  app = create_app(config_class=config or get_config()) # Create app instance using factory function
87
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)
88
107
  port = port or int(os.environ.get("PORT", 8000))
89
108
  debug = debug if debug is not None else app.config.get('DEBUG', True)
90
109
 
91
110
  app.config["LOGGERS"] = logger
92
- 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
93
112
  logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
94
113
 
95
114
  if module:
96
115
  app.config["MODULE"] = module
97
116
  app.config["OFF_LINE"] = False
98
117
  global_config.deck = sys.modules[module]
99
- 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)
100
121
  # global_config.runner = ScriptRunner(globals())
101
122
  else:
102
123
  app.config["OFF_LINE"] = True
@@ -116,3 +137,22 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
116
137
  for log in logger:
117
138
  utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
118
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
@@ -187,7 +187,7 @@ def backend_control(instrument: str=None):
187
187
  return json_output, 400
188
188
  else:
189
189
  return "instrument not exist", 400
190
- return '', 200
190
+ return json_output, 200
191
191
 
192
192
 
193
193
  @control.route("/backend_control", methods=['GET'])
@@ -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
 
@@ -99,25 +100,29 @@ def experiment_builder(instrument=None):
99
100
 
100
101
  deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
101
102
 
102
- functions = []
103
+ functions = {}
103
104
  if deck:
104
105
  deck_variables = global_config.deck_snapshot.keys()
105
106
  else:
106
107
  deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
107
108
  deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
108
- if instrument:
109
- if instrument in ['if', 'while', 'variable', 'wait']:
110
- forms = create_builtin_form(instrument)
109
+ edit_action_info = session.get("edit_action")
110
+ if edit_action_info:
111
+ forms = create_form_from_action(edit_action_info, script=script)
112
+ elif instrument:
113
+ if instrument in ['if', 'while', 'variable', 'wait', 'repeat']:
114
+ forms = create_builtin_form(instrument, script=script)
111
115
  else:
112
116
  if deck:
113
- function_metadata = global_config.deck_snapshot.get(instrument, {})
117
+ functions = global_config.deck_snapshot.get(instrument, {})
114
118
  elif pseudo_deck:
115
- function_metadata = pseudo_deck.get(instrument, {})
116
- functions = {key: data.get('signature', {}) for key, data in function_metadata.items()}
119
+ functions = pseudo_deck.get(instrument, {})
120
+ # print(function_metadata)
121
+ # functions = {key: data.get('signature', {}) for key, data in function_metadata.items()}
117
122
  forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
118
123
  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)
124
+ # all_kwargs = request.form.copy()
125
+ method_name = request.form.get("hidden_name", None)
121
126
  # if method_name is not None:
122
127
  form = forms.get(method_name)
123
128
  kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
@@ -125,29 +130,15 @@ def experiment_builder(instrument=None):
125
130
  if form and form.validate_on_submit():
126
131
  function_name = kwargs.pop("hidden_name")
127
132
  save_data = kwargs.pop('return', '')
128
- variable_kwargs = {}
129
- variable_kwargs_types = {}
130
133
 
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)
134
+ primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
146
135
 
136
+ script.eval_list(kwargs, primitive_arg_types)
137
+ kwargs = script.validate_variables(kwargs)
147
138
  action = {"instrument": instrument, "action": function_name,
148
- "args": {name: arg for (name, arg) in kwargs.items()},
139
+ "args": kwargs,
149
140
  "return": save_data,
150
- 'arg_types': arg_types}
141
+ 'arg_types': primitive_arg_types}
151
142
  script.add_action(action=action)
152
143
  else:
153
144
  flash(form.errors)
@@ -155,9 +146,13 @@ def experiment_builder(instrument=None):
155
146
  elif request.method == 'POST' and "builtin_name" in request.form:
156
147
  kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
157
148
  if forms.validate_on_submit():
149
+ # print(kwargs)
158
150
  logic_type = kwargs.pop('builtin_name')
159
151
  if 'variable' in kwargs:
160
- script.add_variable(**kwargs)
152
+ try:
153
+ script.add_variable(**kwargs)
154
+ except ValueError:
155
+ flash("Invalid variable type")
161
156
  else:
162
157
  script.add_logic_action(logic_type=logic_type, **kwargs)
163
158
  else:
@@ -168,12 +163,13 @@ def experiment_builder(instrument=None):
168
163
  autofill = not autofill
169
164
  forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
170
165
  session['autofill'] = autofill
166
+
171
167
  utils.post_script_file(script)
172
- design_buttons = [create_action_button(i) for i in script.currently_editing_script]
168
+ design_buttons = create_action_button(script)
173
169
  return render_template('experiment_builder.html', off_line=off_line, instrument=instrument, history=deck_list,
174
170
  script=script, defined_variables=deck_variables,
175
171
  local_variables=global_config.defined_variables,
176
- functions=functions, forms=forms, buttons=design_buttons, format_name=format_name,
172
+ forms=forms, buttons=design_buttons, format_name=format_name,
177
173
  use_llm=enable_llm)
178
174
 
179
175
 
@@ -250,10 +246,14 @@ def experiment_run():
250
246
  # module = current_app.config.get('MODULE', '')
251
247
  # deck = sys.modules[module] if module else None
252
248
  # 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}
249
+ design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
254
250
  config_preview = []
255
251
  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'])
252
+ try:
253
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
254
+ except ValueError as e:
255
+ flash(e.__str__())
256
+ return redirect(url_for("design.experiment_builder"))
257
257
  # print(exec_string)
258
258
  config_file = request.args.get("filename")
259
259
  config = []
@@ -306,6 +306,8 @@ def experiment_run():
306
306
  logger=g.logger, socketio=g.socketio, repeat_count=repeat,
307
307
  output_path=datapath
308
308
  )
309
+ if utils.check_config_duplicate(config):
310
+ flash(f"WARNING: Duplicate in config entries.")
309
311
  except Exception as e:
310
312
  flash(e)
311
313
  return render_template('experiment_run.html', script=script.script_dict, filename=filename, dot_py=exec_string,
@@ -490,14 +492,20 @@ def edit_action(uuid: str):
490
492
  script = utils.get_script_file()
491
493
  action = script.find_by_uuid(uuid)
492
494
  session['edit_action'] = action
493
- if request.method == "POST":
495
+
496
+ if request.method == "POST" and action is not None:
497
+ forms = create_form_from_action(action, script=script)
494
498
  if "back" not in request.form:
495
- args = request.form.to_dict()
496
- save_as = args.pop('return', '')
497
- try:
498
- script.update_by_uuid(uuid=uuid, args=args, output=save_as)
499
- except Exception as e:
500
- flash(e.__str__())
499
+ kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
500
+ # print(kwargs)
501
+ if forms and forms.validate_on_submit():
502
+ save_as = kwargs.pop('return', '')
503
+ kwargs = script.validate_variables(kwargs)
504
+ # try:
505
+ script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as)
506
+ # except Exception as e:
507
+ else:
508
+ flash(forms.errors)
501
509
  session.pop('edit_action')
502
510
  return redirect(url_for('design.experiment_builder'))
503
511
 
@@ -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 %}
@@ -72,7 +83,7 @@
72
83
  <!-- <div class="rounded flex-fill" style="height: 30px;background-color: aliceblue">-->
73
84
  <!-- <h6 style=" text-align: center; ">{{ format_name(instrument) }}</h6>-->
74
85
  <!-- </div>-->
75
- {% if instrument in ['if' , 'while' , 'variable' ,'wait'] %}
86
+ {% if instrument in ['if' , 'while' , 'variable' ,'wait', 'repeat'] %}
76
87
  {# form for builtin functions #}
77
88
  <form role="form" method='POST' name="{{instrument}}" action="{{url_for('design.experiment_builder',instrument=instrument)}}" >
78
89
  <div class="form-group">
@@ -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
 
@@ -185,7 +198,7 @@
185
198
  </h5>
186
199
  <div class="accordion-collapse collapse show" id="advanced">
187
200
  <ul class="list-group">
188
- {% for instrument in ['if', 'while', 'variable', 'wait'] %}
201
+ {% for instrument in ['if', 'while', 'variable', 'wait', 'repeat'] %}
189
202
  <form role="form" method='GET' name="device" action="{{url_for('design.experiment_builder',instrument=instrument)}}">
190
203
  <div class="form-group">
191
204
  <button class="list-group-item list-group-item-action" aria-current="true" type="submit">{{instrument}}</button>
@@ -302,7 +315,7 @@
302
315
  {% for button in buttons %}
303
316
  <li id="{{ button['id'] }}" style="list-style-type: none;">
304
317
  <a href="{{ url_for('design.edit_action', uuid=button['uuid']) }}" type="button" class="btn btn-light" style="{{ button['style'] }}">{{ button['label'] }}</a>
305
- {% if not button["instrument"] in ["if","while"] %}
318
+ {% if not button["instrument"] in ["if","while","repeat"] %}
306
319
  <a href="{{ url_for('design.duplicate_action', id=button['id']) }}" type="button" class="btn btn-light"><span class="bi bi-copy"></span></a>
307
320
  {% endif %}
308
321
  <a href="{{ url_for('design.delete_action', id=button['id']) }}" type="button" class="btn btn-light"><span class="bi bi-trash"></span></a>
@@ -403,7 +416,7 @@
403
416
  </div>
404
417
  </div>
405
418
 
406
- {% if instrument and not instrument in ['if' , 'while' , 'variable' ,'wait'] and use_llm %}
419
+ {% if instrument and not instrument in ['if' , 'while' , 'variable' ,'wait', 'repeat'] and use_llm %}
407
420
  <script>
408
421
  const buttonIds = {{ ['generate'] | tojson }};
409
422
  </script>
@@ -50,7 +50,7 @@
50
50
  <form role="form" method='POST' name="run" action="{{url_for('design.experiment_run')}}">
51
51
  <div class="input-group mb-3">
52
52
  <label class="input-group-text" for="repeat">Repeat for </label>
53
- <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="100">
53
+ <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="1">
54
54
  <label class="input-group-text" for="repeat"> times</label>
55
55
  </div>
56
56
  {# {% if not no_deck_warning%}#}
@@ -185,7 +185,7 @@
185
185
  <p><h5>Budget:</h5></p>
186
186
  <div class="input-group mb-3">
187
187
  <label class="input-group-text" for="repeat">Max iteration </label>
188
- <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="100">
188
+ <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">
189
189
  </div>
190
190
  {% if not no_deck_warning%}
191
191
  <div class="input-group mb-3">
@@ -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">
@@ -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