ivoryos 0.1.19__py3-none-any.whl → 0.1.21__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.
@@ -10,13 +10,74 @@ document.addEventListener("DOMContentLoaded", function() {
10
10
  var progressBar = document.getElementById('progress-bar-inner');
11
11
  progressBar.style.width = progress + '%';
12
12
  progressBar.setAttribute('aria-valuenow', progress);
13
+ if (progress === 1) {
14
+ document.getElementById("run-panel").style.display = "none";
15
+ document.getElementById("code-panel").style.display = "block";
13
16
 
17
+ // Optional: Scroll to the code panel
18
+ document.getElementById("code-panel").scrollIntoView({ behavior: "smooth" });
19
+ }
14
20
  if (progress === 100) {
15
21
  // Remove animation and set green color when 100% is reached
16
22
  progressBar.classList.remove('progress-bar-animated');
17
23
  progressBar.classList.add('bg-success'); // Bootstrap class for green color
24
+ setTimeout(() => {
25
+ document.getElementById("code-panel").style.display = "none";
26
+ document.getElementById("run-panel").style.display = "block";
27
+ }, 1000); // Small delay to let users see the completion
28
+ }
29
+ });
30
+
31
+ socket.on('error', function(errorData) {
32
+ console.error("Error received:", errorData);
33
+ var progressBar = document.getElementById('progress-bar-inner');
34
+
35
+ progressBar.classList.remove('bg-success');
36
+ progressBar.classList.add('bg-danger'); // Red color for error
37
+ // Show error modal
38
+ var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
39
+ document.getElementById('error-message').innerText = "An error occurred: " + errorData.message;
40
+ errorModal.show();
41
+
42
+ });
43
+
44
+ // Handle Pause/Resume Button
45
+ document.getElementById('pause-resume').addEventListener('click', function() {
46
+ socket.emit('pause');
47
+ console.log('Pause/Resume is toggled.');
48
+ var button = this;
49
+ var icon = button.querySelector("i");
50
+
51
+ // Toggle Pause and Resume
52
+ if (icon.classList.contains("bi-pause-circle")) {
53
+ icon.classList.remove("bi-pause-circle");
54
+ icon.classList.add("bi-play-circle");
55
+ button.innerHTML = '<i class="bi bi-play-circle"></i>';
56
+ button.setAttribute("title", "Resume execution");
57
+ } else {
58
+ icon.classList.remove("bi-play-circle");
59
+ icon.classList.add("bi-pause-circle");
60
+ button.innerHTML = '<i class="bi bi-pause-circle"></i>';
61
+ button.setAttribute("title", "Pause execution");
18
62
  }
19
63
  });
64
+
65
+ // Handle Modal Buttons
66
+ document.getElementById('continue-btn').addEventListener('click', function() {
67
+ socket.emit('pause'); // Resume execution
68
+ console.log("Execution resumed.");
69
+ });
70
+
71
+ document.getElementById('stop-btn').addEventListener('click', function() {
72
+ socket.emit('pause'); // Resume execution
73
+ socket.emit('abort_current'); // Stop execution
74
+ console.log("Execution stopped.");
75
+
76
+ // Reset UI back to initial state
77
+ document.getElementById("code-panel").style.display = "none";
78
+ document.getElementById("run-panel").style.display = "block";
79
+ });
80
+
20
81
  socket.on('log', function(data) {
21
82
  var logMessage = data.message;
22
83
  console.log(logMessage);
@@ -39,24 +100,16 @@ document.addEventListener("DOMContentLoaded", function() {
39
100
  }
40
101
  });
41
102
 
42
- document.getElementById('pause-resume').addEventListener('click', function() {
103
+ socket.on('execution', function(data) {
104
+ // Remove highlighting from all lines
105
+ document.querySelectorAll('pre code').forEach(el => el.style.backgroundColor = '');
43
106
 
44
- socket.emit('pause');
45
- console.log('Pause/Resume is toggled.');
46
- var button = this;
47
- var icon = button.querySelector("i");
107
+ // Get the currently executing line and highlight it
108
+ let executingLine = document.getElementById(data.section);
109
+ if (executingLine) {
110
+ executingLine.style.backgroundColor = '#cce5ff'; // Highlight
111
+ executingLine.style.transition = 'background-color 0.3s ease-in-out';
48
112
 
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
113
  }
61
114
  });
62
115
  });
@@ -1,36 +1,105 @@
1
- $(document).ready(function(){
2
- function slideout(){
3
- setTimeout(function(){
4
- $("#response").slideUp("slow", function () {});
5
- }, 2000);
6
- }
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
+ slideout();
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
+ });
7
58
 
8
- $("#response").hide();
9
-
10
- $(function() {
11
- $("#list ul").sortable({
12
- cancel: ".unsortable",
13
- opacity: 0.8,
14
- cursor: 'move',
15
- update: function() {
16
- var item_order = [];
17
- $('ul.reorder li').each(function() {
18
- item_order.push($(this).attr("id"));
19
- });
20
- var order_string = 'order=' + item_order.join(',');
21
-
22
- $.ajax({
23
- method: "POST",
24
- url: updateListUrl, // updateListUrl should be set dynamically in your HTML template
25
- data: order_string,
26
- cache: false,
27
- success: function(data){
28
- $("#response").html(data);
29
- $("#response").slideDown('slow');
30
- slideout();
31
- }
32
- });
33
- }
34
- });
59
+ $("#list ul, .canvas").on("dragleave", function () {
60
+ $(".drop-placeholder").remove(); // Remove placeholder on leave
35
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
+ }
36
105
  });
ivoryos/static/style.css CHANGED
@@ -199,4 +199,13 @@ hr.vertical {
199
199
  color: white;
200
200
  font-size: 24px;
201
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 */
202
211
  }
@@ -54,9 +54,9 @@
54
54
  <li class="nav-item">
55
55
  <a class="nav-link" href="{{ url_for('control.controllers_home') }}">Temp Devices</a></li>
56
56
  </li>
57
- <li class="nav-item">
58
- <a class="nav-link" href="{{ url_for('main.help_info') }}">About</a>
59
- </li>
57
+ {# <li class="nav-item">#}
58
+ {# <a class="nav-link" href="{{ url_for('main.help_info') }}">About</a>#}
59
+ {# </li>#}
60
60
  {% if plugins %}
61
61
  {% for plugin in plugins %}
62
62
  <li class="nav-item">
@@ -196,28 +196,37 @@ class Script(db.Model):
196
196
  for i in self.stypes:
197
197
  self._sort(i)
198
198
 
199
- def add_action(self, action: dict):
199
+ def add_action(self, action: dict, insert_position=None):
200
200
  current_len = len(self.currently_editing_script)
201
201
  action_to_add = action.copy()
202
202
  action_to_add['id'] = current_len + 1
203
203
  action_to_add['uuid'] = uuid.uuid4().fields[-1]
204
204
  self.currently_editing_script.append(action_to_add)
205
- self.currently_editing_order.append(str(current_len + 1))
205
+ self._insert_action(insert_position, current_len)
206
206
  self.update_time_stamp()
207
207
 
208
- def add_variable(self, statement, variable, type):
208
+ def add_variable(self, statement, variable, type, insert_position=None):
209
209
  variable = self.validate_function_name(variable)
210
210
  convert_type = getattr(builtins, type)
211
211
  statement = convert_type(statement)
212
212
  current_len = len(self.currently_editing_script)
213
213
  uid = uuid.uuid4().fields[-1]
214
- action_list = [{"id": current_len + 1, "instrument": 'variable', "action": variable,
214
+ action = {"id": current_len + 1, "instrument": 'variable', "action": variable,
215
215
  "args": {"statement": 'None' if statement == '' else statement}, "return": '', "uuid": uid,
216
- "arg_types": {"statement": type}}]
217
- self.currently_editing_script.extend(action_list)
218
- self.currently_editing_order.extend([str(current_len + i + 1) for i in range(len(action_list))])
216
+ "arg_types": {"statement": type}}
217
+ self.currently_editing_script.append(action)
218
+ self._insert_action(insert_position, current_len)
219
219
  self.update_time_stamp()
220
220
 
221
+ def _insert_action(self, insert_position, current_len, action_len:int=1):
222
+
223
+ if insert_position is None:
224
+ self.currently_editing_order.extend([str(current_len + i + 1) for i in range(action_len)])
225
+ else:
226
+ index = int(insert_position) - 1
227
+ self.currently_editing_order[index:index] = [str(current_len + i + 1) for i in range(action_len)]
228
+ self.sort_actions()
229
+
221
230
  def get_added_variables(self):
222
231
  added_variables: Dict[str, str] = {action["action"]: action["arg_types"]["statement"] for action in
223
232
  self.currently_editing_script if action["instrument"] == "variable"}
@@ -251,7 +260,7 @@ class Script(db.Model):
251
260
  kwargs[key] = f"#{self.validate_function_name(value[1:])}"
252
261
  return kwargs
253
262
 
254
- def add_logic_action(self, logic_type: str, statement):
263
+ def add_logic_action(self, logic_type: str, statement, insert_position=None):
255
264
  current_len = len(self.currently_editing_script)
256
265
  uid = uuid.uuid4().fields[-1]
257
266
  logic_dict = {
@@ -291,7 +300,7 @@ class Script(db.Model):
291
300
  }
292
301
  action_list = logic_dict[logic_type]
293
302
  self.currently_editing_script.extend(action_list)
294
- self.currently_editing_order.extend([str(current_len + i + 1) for i in range(len(action_list))])
303
+ self._insert_action(insert_position, current_len, len(action_list))
295
304
  self.update_time_stamp()
296
305
 
297
306
  def delete_action(self, id: int):
@@ -390,6 +399,23 @@ class Script(db.Model):
390
399
  string += "\t"
391
400
  return string
392
401
 
402
+ def convert_to_lines(self, exec_str_collection: dict):
403
+ """
404
+ Parse a dictionary of script functions and extract function body lines.
405
+
406
+ :param exec_str_collection: Dictionary containing script types and corresponding function strings.
407
+ :return: A dict containing script types as keys and lists of function body lines as values.
408
+ """
409
+ line_collection = {}
410
+ for stype, func_str in exec_str_collection.items():
411
+ module = ast.parse(func_str)
412
+ func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
413
+
414
+ # Extract function body as source lines
415
+ line_collection[stype] = [ast.unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
416
+ # print(line_collection[stype])
417
+ return line_collection
418
+
393
419
  def compile(self, script_path=None):
394
420
  """
395
421
  Compile the current script to a Python file.
@@ -401,8 +427,9 @@ class Script(db.Model):
401
427
  exec_str_collection = {}
402
428
 
403
429
  for i in self.stypes:
404
- func_str = self._generate_function_header(run_name, i) + self._generate_function_body(i)
405
- exec_str_collection[i] = func_str
430
+ if self.script_dict[i]:
431
+ func_str = self._generate_function_header(run_name, i) + self._generate_function_body(i)
432
+ exec_str_collection[i] = func_str
406
433
  if script_path:
407
434
  self._write_to_file(script_path, run_name, exec_str_collection)
408
435
 
@@ -428,13 +455,14 @@ class Script(db.Model):
428
455
  configure = [param + f":{param_type}" if not param_type == "any" else "" for param, param_type in
429
456
  config_type.items()]
430
457
 
431
- function_header = f"def {run_name}_{stype}("
458
+ script_type = f"_{stype}" if stype != "script" else ""
459
+ function_header = f"def {run_name}{script_type}("
432
460
 
433
461
  if stype == "script":
434
462
  function_header += ", ".join(configure)
435
463
 
436
464
  function_header += "):"
437
- function_header += self.indent(1) + f"global {run_name}_{stype}"
465
+ # function_header += self.indent(1) + f"global {run_name}_{stype}"
438
466
  return function_header
439
467
 
440
468
  def _generate_function_body(self, stype):
@@ -474,6 +502,9 @@ class Script(db.Model):
474
502
  return f"{self.indent(indent_unit)}time.sleep({statement})", indent_unit
475
503
  elif instrument == 'repeat':
476
504
  return self._process_repeat(indent_unit, action_name, statement, next_action)
505
+ #todo
506
+ # elif instrument == 'registered_workflows':
507
+ # return inspect.getsource(my_function)
477
508
  else:
478
509
  return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data)
479
510
 
ivoryos/utils/form.py CHANGED
@@ -7,6 +7,10 @@ from flask_wtf import FlaskForm
7
7
  from wtforms import StringField, FloatField, HiddenField, BooleanField, IntegerField
8
8
  import inspect
9
9
 
10
+ from ivoryos.utils.db_models import Script
11
+ from ivoryos.utils.global_config import GlobalConfig
12
+
13
+ global_config = GlobalConfig()
10
14
 
11
15
  def find_variable(data, script):
12
16
  """
@@ -345,6 +349,13 @@ def create_form_from_action(action: dict, script=None, design=True):
345
349
  setattr(DynamicForm, 'return', return_value)
346
350
  return DynamicForm()
347
351
 
352
+ def create_all_builtin_forms(script):
353
+ all_builtin_forms = {}
354
+ for logic_name in ['if', 'while', 'variable', 'wait', 'repeat']:
355
+ # signature = info.get('signature', {})
356
+ form_class = create_builtin_form(logic_name, script)
357
+ all_builtin_forms[logic_name] = form_class()
358
+ return all_builtin_forms
348
359
 
349
360
  def create_builtin_form(logic_type, script):
350
361
  """
@@ -379,14 +390,51 @@ def create_builtin_form(logic_type, script):
379
390
  render_kw=render_kwargs)
380
391
  type_field = SelectField(
381
392
  'Select Input Type',
382
- choices=[('int', 'Integer'), ('float', 'Float'), ('str', 'String')],
393
+ choices=[('int', 'Integer'), ('float', 'Float'), ('str', 'String'), ('bool', 'Boolean')],
383
394
  default='str' # Optional default value
384
395
  )
385
396
  setattr(BuiltinFunctionForm, "variable", variable_field)
386
397
  setattr(BuiltinFunctionForm, "type", type_field)
387
398
  hidden_field = HiddenField(name=f'builtin_name', render_kw={"value": f'{logic_type}'})
388
399
  setattr(BuiltinFunctionForm, "builtin_name", hidden_field)
389
- return BuiltinFunctionForm()
400
+ return BuiltinFunctionForm
401
+
402
+
403
+ def get_method_from_workflow(function_string):
404
+ """Creates a function from a string and assigns it a new name."""
405
+
406
+ namespace = {}
407
+ exec(function_string, globals(), namespace) # Execute the string in a safe namespace
408
+ func_name = next(iter(namespace))
409
+ # Get the function name dynamically
410
+ return namespace[func_name]
411
+
412
+
413
+ def create_workflow_forms(script, autofill: bool = False, design: bool = False):
414
+ workflow_forms = {}
415
+ functions = {}
416
+ class RegisteredWorkflows:
417
+ pass
418
+
419
+ deck_name = script.deck
420
+ workflows = Script.query.filter(Script.deck==deck_name, Script.name != script.name).all()
421
+ for workflow in workflows:
422
+ compiled_strs = workflow.compile().get('script', "")
423
+ method = get_method_from_workflow(compiled_strs)
424
+ functions[workflow.name] = dict(signature=inspect.signature(method), docstring=inspect.getdoc(method))
425
+ setattr(RegisteredWorkflows, workflow.name, method)
426
+
427
+ form_class = create_form_for_method(method, autofill, script, design)
428
+
429
+ hidden_method_name = HiddenField(name=f'hidden_name', description="",
430
+ render_kw={"value": f'{workflow.name}'})
431
+ if design:
432
+ return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
433
+ setattr(form_class, 'return', return_value)
434
+ setattr(form_class, 'workflow_name', hidden_method_name)
435
+ workflow_forms[workflow.name] = form_class()
436
+ global_config.registered_workflows = RegisteredWorkflows
437
+ return workflow_forms, functions
390
438
 
391
439
 
392
440
  def create_action_button(script, stype=None):
@@ -8,6 +8,7 @@ class GlobalConfig:
8
8
  if cls._instance is None:
9
9
  cls._instance = super(GlobalConfig, cls).__new__(cls, *args, **kwargs)
10
10
  cls._instance._deck = None
11
+ cls._instance._registered_workflows = None
11
12
  cls._instance._agent = None
12
13
  cls._instance._defined_variables = {}
13
14
  cls._instance._api_variables = set()
@@ -24,6 +25,15 @@ class GlobalConfig:
24
25
  if self._deck is None:
25
26
  self._deck = value
26
27
 
28
+ @property
29
+ def registered_workflows(self):
30
+ return self._registered_workflows
31
+
32
+ @registered_workflows.setter
33
+ def registered_workflows(self, value):
34
+ if self._registered_workflows is None:
35
+ self._registered_workflows = value
36
+
27
37
 
28
38
  @property
29
39
  def deck_snapshot(self):