meta_workflows 0.9.19 → 0.9.20

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf7568849f397d177f74890b1036e8a8f6ab845ff7f68b67f53641688637ad8b
4
- data.tar.gz: da332e7bd731c441e4a80ffa53b5ba398edca8843d459cc11aa51af81de7caa6
3
+ metadata.gz: 3b91afe608b3c5260eebee5a4cc1211621c85171cf2ddc3740f5c695bc6df73f
4
+ data.tar.gz: cbafaa30a15e992936bdbe66ec444fb15740bbac999c2137e11f67d26870eea0
5
5
  SHA512:
6
- metadata.gz: cfdca5467ebd4caa0872c2bde60cc7b7b800d7caabf78673711fb0480e7f297e583f276527f95598c017d097617aff4648db156226865f82b1791aa9f9c1d123
7
- data.tar.gz: b68da4e79a390b42dc74952987f09822093424190d2373d87a0a49067a226ff04e55325db5d1c6f501f3fd8341c16fd7a85a9d4ef70041e8dbcb32aeb58f19bf
6
+ metadata.gz: '0886296d806086437b57857c1e509ea76a6db6c784df7bee23ff54428220d819e94265083165a0e33a17715f9ab00a9f0efd0a553d365c21b5b68a93b7c56c4e'
7
+ data.tar.gz: 76b74c3f7508be1ee96547583adaf43a13f41c8af6ba74023fdcd3cafd0cd59c34ff535eeb2490ee4ba0c03c23c75bfb6ed248630d179938e9dedd724fd50add
@@ -0,0 +1,18 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ['form', 'radioInput', 'checkboxInput', 'sliderInput'];
5
+
6
+ connect() {
7
+ this.submitted = false;
8
+ }
9
+
10
+ handleSubmit() {
11
+ if (this.submitted) {
12
+ return;
13
+ }
14
+
15
+ this.submitted = true;
16
+ this.formTarget.requestSubmit();
17
+ }
18
+ }
@@ -824,3 +824,224 @@
824
824
  .lexi-error-content .lexi-loader-ellipses {
825
825
  margin-left: 0.375rem;
826
826
  }
827
+
828
+ /* Structured Input Styles */
829
+ .structured-input-container {
830
+ padding: 1rem;
831
+ background-color: rgba(255, 255, 255, 0.9);
832
+ border-radius: 0.75rem;
833
+ border: 1px solid var(--gray-300);
834
+ margin-bottom: 1rem;
835
+ }
836
+
837
+ .structured-input-options {
838
+ display: flex;
839
+ flex-direction: column;
840
+ gap: 0.75rem;
841
+ }
842
+
843
+ /* Radio Button Styles */
844
+ .structured-radio-option {
845
+ display: flex;
846
+ align-items: center;
847
+ gap: 0.75rem;
848
+ padding: 0.75rem 1rem;
849
+ border: 1px solid var(--gray-300);
850
+ border-radius: 0.5rem;
851
+ background-color: white;
852
+ cursor: pointer;
853
+ transition: all 0.2s ease;
854
+ }
855
+
856
+ .structured-radio-option:hover {
857
+ background-color: var(--gray-50);
858
+ border-color: var(--purple-300);
859
+ }
860
+
861
+ .structured-radio-option:has(.structured-radio-input:checked) {
862
+ background-color: var(--purple-50);
863
+ border-color: var(--purple-500);
864
+ }
865
+
866
+ .structured-radio-input {
867
+ width: 1.25rem;
868
+ height: 1.25rem;
869
+ accent-color: var(--purple-600);
870
+ cursor: pointer;
871
+ }
872
+
873
+ .structured-radio-label {
874
+ font-size: 1rem;
875
+ font-weight: 500;
876
+ color: var(--gray-800);
877
+ cursor: pointer;
878
+ flex: 1;
879
+ }
880
+
881
+ /* Checkbox Styles */
882
+ .structured-checkbox-option {
883
+ display: flex;
884
+ align-items: center;
885
+ gap: 0.75rem;
886
+ padding: 0.75rem 1rem;
887
+ border: 1px solid var(--gray-300);
888
+ border-radius: 0.5rem;
889
+ background-color: white;
890
+ cursor: pointer;
891
+ transition: all 0.2s ease;
892
+ }
893
+
894
+ .structured-checkbox-option:hover {
895
+ background-color: var(--gray-50);
896
+ border-color: var(--purple-300);
897
+ }
898
+
899
+ .structured-checkbox-option:has(.structured-checkbox-input:checked) {
900
+ background-color: var(--purple-50);
901
+ border-color: var(--purple-500);
902
+ }
903
+
904
+ .structured-checkbox-input {
905
+ width: 1.25rem;
906
+ height: 1.25rem;
907
+ accent-color: var(--purple-600);
908
+ cursor: pointer;
909
+ }
910
+
911
+ .structured-checkbox-label {
912
+ font-size: 1rem;
913
+ font-weight: 500;
914
+ color: var(--gray-800);
915
+ cursor: pointer;
916
+ flex: 1;
917
+ }
918
+
919
+ /* Slider Styles */
920
+ .structured-slider-container {
921
+ padding: 1rem;
922
+ }
923
+
924
+ .structured-slider-labels {
925
+ display: flex;
926
+ justify-content: space-between;
927
+ margin-bottom: 1rem;
928
+ font-size: 0.875rem;
929
+ color: var(--gray-600);
930
+ font-weight: 500;
931
+ }
932
+
933
+ .structured-slider-wrapper {
934
+ display: flex;
935
+ flex-direction: column;
936
+ gap: 0.5rem;
937
+ }
938
+
939
+ .structured-slider-input {
940
+ width: 100%;
941
+ height: 0.5rem;
942
+ border-radius: 0.25rem;
943
+ background: var(--gray-200);
944
+ outline: none;
945
+ -webkit-appearance: none;
946
+ appearance: none;
947
+ cursor: pointer;
948
+ }
949
+
950
+ .structured-slider-input::-webkit-slider-thumb {
951
+ -webkit-appearance: none;
952
+ appearance: none;
953
+ width: 1.5rem;
954
+ height: 1.5rem;
955
+ border-radius: 50%;
956
+ background: var(--purple-600);
957
+ cursor: pointer;
958
+ border: 2px solid white;
959
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
960
+ transition: all 0.2s ease;
961
+ }
962
+
963
+ .structured-slider-input::-webkit-slider-thumb:hover {
964
+ background: var(--purple-700);
965
+ transform: scale(1.1);
966
+ }
967
+
968
+ .structured-slider-input::-moz-range-thumb {
969
+ width: 1.5rem;
970
+ height: 1.5rem;
971
+ border-radius: 50%;
972
+ background: var(--purple-600);
973
+ cursor: pointer;
974
+ border: 2px solid white;
975
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
976
+ transition: all 0.2s ease;
977
+ }
978
+
979
+ .structured-slider-input::-moz-range-thumb:hover {
980
+ background: var(--purple-700);
981
+ transform: scale(1.1);
982
+ }
983
+
984
+ .structured-slider-value {
985
+ text-align: center;
986
+ font-size: 1.125rem;
987
+ font-weight: 600;
988
+ color: var(--purple-700);
989
+ padding: 0.5rem;
990
+ background-color: var(--purple-50);
991
+ border-radius: 0.375rem;
992
+ border: 1px solid var(--purple-200);
993
+ }
994
+
995
+ /* Structured Input Submit Button */
996
+ .structured-submit-button {
997
+ width: 3rem;
998
+ height: 3rem;
999
+ border-radius: 50%;
1000
+ display: flex;
1001
+ align-items: center;
1002
+ justify-content: center;
1003
+ background-color: var(--purple-600);
1004
+ color: white;
1005
+ transition: all 0.2s;
1006
+ border: none;
1007
+ cursor: pointer;
1008
+ margin-top: 1rem;
1009
+ align-self: flex-end;
1010
+ }
1011
+
1012
+ .structured-submit-button:hover {
1013
+ background-color: var(--purple-700);
1014
+ transform: scale(1.05);
1015
+ }
1016
+
1017
+ .structured-submit-button:disabled {
1018
+ background-color: var(--gray-400);
1019
+ cursor: not-allowed;
1020
+ transform: none;
1021
+ }
1022
+
1023
+ /* Structured Input Validation Styles */
1024
+ .structured-input-container[data-invalid="true"] {
1025
+ border-color: var(--red-500);
1026
+ background-color: var(--red-50);
1027
+ }
1028
+
1029
+ .structured-input-error {
1030
+ color: var(--red-600);
1031
+ font-size: 0.875rem;
1032
+ margin-top: 0.5rem;
1033
+ font-weight: 500;
1034
+ }
1035
+
1036
+ /* Screen reader only class for accessibility */
1037
+ .sr-only {
1038
+ position: absolute;
1039
+ width: 1px;
1040
+ height: 1px;
1041
+ padding: 0;
1042
+ margin: -1px;
1043
+ overflow: hidden;
1044
+ clip: rect(0, 0, 0, 0);
1045
+ white-space: nowrap;
1046
+ border: 0;
1047
+ }
@@ -9,10 +9,10 @@ module MetaWorkflows
9
9
  def update
10
10
  auto_advancing = should_auto_advance?
11
11
 
12
- if params[:advance].present? || auto_advancing
12
+ if params[:manual_advance].present? || auto_advancing
13
13
  @workflow_execution.increment_step
14
14
  render_loader_stream
15
- process_human_input(auto_advancing:, manual_advancing: params[:advance].present?)
15
+ process_human_input(auto_advancing:, manual_advancing: params[:manual_advance].present?)
16
16
  else
17
17
  render_response_form_stream
18
18
  process_human_input
@@ -23,6 +23,7 @@ module MetaWorkflows
23
23
 
24
24
  def should_auto_advance?
25
25
  step_repetitions = current_step_repetitions
26
+ return true if params[:auto_advance].present?
26
27
  return false unless step_repetitions
27
28
 
28
29
  new_repetition = @workflow_step.increment_repetition!
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ class StructuredHumansController < MetaController
5
+ include MetaWorkflows::MetaWorkflowsHelper
6
+ include MetaWorkflows::Streamable
7
+ before_action :set_workflow_data, only: [:update]
8
+
9
+ def update
10
+ render_loader_stream
11
+ process_structured_human_input
12
+ @workflow_execution.increment_step
13
+ end
14
+
15
+ private
16
+
17
+ def process_structured_human_input
18
+ MetaWorkflows::HumanInputJob.perform_later(
19
+ user_id: current_user.id,
20
+ record: @record,
21
+ auto_advancing: true,
22
+ manual_advancing: false,
23
+ params: {
24
+ inputs: extract_structured_inputs,
25
+ chat_id: params[:chat_id],
26
+ prompt_id: @prompt_id
27
+ }
28
+ )
29
+ end
30
+
31
+ def extract_structured_inputs
32
+ step_data = @workflow_execution.step_data(@workflow_execution.current_step)
33
+ structured_input_config = step_data&.dig('structured_input')
34
+ return params[:message] unless structured_input_config
35
+
36
+ case structured_input_config['type']
37
+ when 'single_choice'
38
+ params[:single_choice_selection]
39
+ when 'multiple_choice'
40
+ params[:multiple_choice_selections]
41
+ when 'range'
42
+ params[:range_value]
43
+ else
44
+ params[:message]
45
+ end
46
+ end
47
+
48
+ def render_loader_stream
49
+ respond_to do |format|
50
+ format.turbo_stream do
51
+ render turbo_stream: [
52
+ build_loader_stream(record: @record, workflow_execution: @workflow_execution),
53
+ build_response_form_stream(record: @record, workflow_execution: @workflow_execution,
54
+ response_enabled: false, responding: false, chat_id: params[:chat_id])
55
+ ]
56
+ end
57
+ end
58
+ end
59
+
60
+ def render_response_form_stream
61
+ respond_to do |format|
62
+ format.turbo_stream do
63
+ render turbo_stream: build_response_form_stream(
64
+ record: @record,
65
+ workflow_execution: @workflow_execution,
66
+ response_enabled: true,
67
+ responding: true,
68
+ chat_id: params[:chat_id]
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module MetaWorkflows
4
4
  module MetaWorkflowsHelper
5
- def target_frame_id(record, form: nil, loader: nil)
5
+ def target_frame_id(record, form: nil, loader: nil, structured_input: nil)
6
6
  base_id = "#{record.class.name.downcase}_#{record.id}"
7
7
 
8
8
  return "#{base_id}_loader" if loader
9
9
  return "#{base_id}_form" if form
10
+ return "#{base_id}_structured_input" if structured_input
10
11
 
11
12
  base_id
12
13
  end
@@ -27,6 +28,10 @@ module MetaWorkflows
27
28
  'meta_workflows/response_form_lexi'
28
29
  end
29
30
 
31
+ def meta_structured_input
32
+ 'meta_workflows/structured_input'
33
+ end
34
+
30
35
  def meta_response
31
36
  'meta_workflows/response_lexi'
32
37
  end
@@ -5,8 +5,11 @@ module MetaWorkflows
5
5
  include MetaWorkflows::MetaWorkflowsHelper
6
6
  queue_as :default
7
7
 
8
+ attr_accessor :current_step_data, :structured_input_config, :is_structured_input
9
+
8
10
  def perform(user_id:, record:, params:, auto_advancing: false, manual_advancing: false)
9
11
  setup(user_id, record, auto_advancing, manual_advancing)
12
+ set_workflow_step_variables
10
13
  conversation = initialize_conversation(params)
11
14
  process_and_broadcast(conversation) unless manual_advancing
12
15
  advance_workflow(params) if advancing?
@@ -14,6 +17,12 @@ module MetaWorkflows
14
17
 
15
18
  private
16
19
 
20
+ def set_workflow_step_variables
21
+ @current_step_data = workflow_execution&.step_data(workflow_execution.current_step)
22
+ @structured_input_config = current_step_data&.dig('structured_input')
23
+ @is_structured_input = structured_input_config.present? && current_step_data&.dig('action') == 'structured_human'
24
+ end
25
+
17
26
  def broadcast_response(chat, full_response)
18
27
  user_messages = chat.messages.where(role: 'user').order(:created_at)
19
28
  messages = chat.messages.order(:created_at)
@@ -42,7 +51,6 @@ module MetaWorkflows
42
51
  end
43
52
 
44
53
  def broadcast_form(chat)
45
- workflow_execution = active_workflow_execution
46
54
  Turbo::StreamsChannel.broadcast_replace_to(
47
55
  turbo_stream_name(record),
48
56
  target: target_frame_id(record, form: true),
@@ -51,7 +59,22 @@ module MetaWorkflows
51
59
  workflow_execution_id: workflow_execution&.id,
52
60
  response_enabled: true,
53
61
  chat_id: chat.id,
54
- step_has_repetitions: current_step_has_repetitions?(workflow_execution) }
62
+ step_has_repetitions: current_step_has_repetitions?(workflow_execution),
63
+ is_structured_input: is_structured_input }
64
+ )
65
+ end
66
+
67
+ def broadcast_structured_input(chat)
68
+ Turbo::StreamsChannel.broadcast_replace_to(
69
+ turbo_stream_name(record),
70
+ target: target_frame_id(record, structured_input: true),
71
+ partial: meta_structured_input,
72
+ locals: {
73
+ record: record,
74
+ chat_id: chat.id,
75
+ structured_input_config: structured_input_config,
76
+ is_structured_input: is_structured_input
77
+ }
55
78
  )
56
79
  end
57
80
 
@@ -5,7 +5,8 @@ module MetaWorkflows
5
5
  include MetaWorkflows::Concerns::ErrorHandling
6
6
  include MetaWorkflows::Streamable
7
7
 
8
- attr_accessor :chat, :inputs, :full_response, :user_id, :record, :auto_advancing, :manual_advancing
8
+ attr_accessor :chat, :inputs, :full_response, :user_id, :record, :auto_advancing, :manual_advancing,
9
+ :workflow_execution
9
10
 
10
11
  private
11
12
 
@@ -15,6 +16,7 @@ module MetaWorkflows
15
16
  @full_response = +''
16
17
  @auto_advancing = auto_advancing
17
18
  @manual_advancing = manual_advancing
19
+ @workflow_execution = active_workflow_execution
18
20
  end
19
21
 
20
22
  def initialize_conversation(params)
@@ -28,7 +30,6 @@ module MetaWorkflows
28
30
  def initialize_new_conversation(params)
29
31
  inputs = params.symbolize_keys[:inputs]
30
32
 
31
- workflow_execution = active_workflow_execution
32
33
  current_step = workflow_execution.workflow_steps&.find_by(step: workflow_execution.current_step)
33
34
  @chat = current_step&.chat
34
35
 
@@ -45,7 +46,6 @@ module MetaWorkflows
45
46
  end
46
47
 
47
48
  def process_and_broadcast(conversation)
48
- workflow_execution = active_workflow_execution
49
49
  workflow_step = current_workflow_step(workflow_execution)
50
50
 
51
51
  begin
@@ -72,13 +72,16 @@ module MetaWorkflows
72
72
  def finalize_conversation(conversation)
73
73
  chat.update!(conversation_id: conversation.id) if chat.conversation_id.blank?
74
74
  persist_messages_to_history
75
- broadcast_form(chat) unless auto_advancing
75
+ return if auto_advancing
76
+
77
+ broadcast_form(chat)
78
+ broadcast_structured_input(chat)
76
79
  end
77
80
 
78
81
  def persist_messages_to_history
79
82
  MetaWorkflows::MessageHistoryService.new(
80
83
  chat: chat,
81
- workflow_execution: active_workflow_execution
84
+ workflow_execution: workflow_execution
82
85
  ).persist_messages_to_history
83
86
  end
84
87
 
@@ -92,6 +95,10 @@ module MetaWorkflows
92
95
  raise NotImplementedError
93
96
  end
94
97
 
98
+ def broadcast_structured_input(chat)
99
+ raise NotImplementedError
100
+ end
101
+
95
102
  def broadcast_response(chat, full_response)
96
103
  raise NotImplementedError
97
104
  end
@@ -0,0 +1,24 @@
1
+ <div class="structured-input-container"
2
+ role="group"
3
+ aria-labelledby="structured-checkbox-legend">
4
+ <legend id="structured-checkbox-legend" class="sr-only">Select one or more options</legend>
5
+ <div class="structured-input-options">
6
+ <% options.each_with_index do |option, index| %>
7
+ <div class="structured-checkbox-option">
8
+ <input
9
+ type="checkbox"
10
+ id="multiple_choice_<%= index %>"
11
+ name="multiple_choice_selections[]"
12
+ value="<%= option['value'] %>"
13
+ class="structured-checkbox-input"
14
+ data-action="change->meta-workflows--structured-form-submit#handleSubmit"
15
+ aria-describedby="structured-checkbox-help"
16
+ >
17
+ <label for="multiple_choice_<%= index %>" class="structured-checkbox-label">
18
+ <%= option['label'] %>
19
+ </label>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+ <div id="structured-checkbox-help" class="sr-only">Use space to select/deselect options</div>
24
+ </div>
@@ -43,6 +43,10 @@
43
43
  <%# Input area (fixed at bottom) %>
44
44
  <div class="lexi-chat-alpha-input-area">
45
45
  <div class="lexi-chat-alpha-input-container">
46
+ <!-- Structured Input (always rendered, conditionally displays content) -->
47
+ <%= render meta_structured_input, record: local_assigns[:record], structured_input_config: false, is_structured_input: false %>
48
+
49
+ <!-- Regular Form Input (always present) -->
46
50
  <%= render partial: meta_response_form, locals: {record: local_assigns[:record], response_enabled: true, workflow_execution_id: active_execution&.id, chat_id: active_execution&.workflow_steps&.last&.chat&.id, step_has_repetitions: true } %>
47
51
  </div>
48
52
  </div>
@@ -0,0 +1,26 @@
1
+ <div class="structured-input-container"
2
+ role="radiogroup"
3
+ aria-labelledby="structured-radio-legend"
4
+ aria-required="true">
5
+ <legend id="structured-radio-legend" class="sr-only">Select one option</legend>
6
+ <div class="structured-input-options">
7
+ <% options.each_with_index do |option, index| %>
8
+ <div class="structured-radio-option">
9
+ <input
10
+ type="radio"
11
+ id="single_choice_<%= index %>"
12
+ name="single_choice_selection"
13
+ value="<%= option['value'] %>"
14
+ class="structured-radio-input"
15
+ data-action="change->meta-workflows--structured-form-submit#handleSubmit"
16
+ aria-describedby="structured-radio-help"
17
+ required
18
+ >
19
+ <label for="single_choice_<%= index %>" class="structured-radio-label">
20
+ <%= option['label'] %>
21
+ </label>
22
+ </div>
23
+ <% end %>
24
+ </div>
25
+ <div id="structured-radio-help" class="sr-only">Use arrow keys to navigate between options</div>
26
+ </div>
@@ -1,44 +1,47 @@
1
1
  <%= turbo_frame_tag target_frame_id(record, form: true) do %>
2
2
  <div>
3
- <%= form_with url: (workflow_execution_id.present? ? meta_workflows.human_path(workflow_execution_id) : "#"),
3
+ <%= form_with url: workflow_execution_id.present? ? meta_workflows.human_path(workflow_execution_id) : "#",
4
4
  method: :patch,
5
5
  id: "#{target_frame_id(record, form: true)}_lexi",
6
6
  data: { controller: "meta-workflows--lexi-form-submit", "meta-workflows--lexi-form-submit-target": "form" } do |form| %>
7
7
  <%= form.hidden_field :chat_id, value: chat_id %>
8
+ <% if local_assigns[:is_structured_input] %>
9
+ <%= form.hidden_field :auto_advance, value: true %>
10
+ <% end %>
8
11
  <fieldset>
9
12
  <div class="lexi-form-container">
10
13
  <!-- Input Container -->
11
14
  <div class="lexi-input-container lexi-input-max-height">
12
- <!-- Input area with icons -->
13
- <div class="lexi-textarea-wrapper">
14
- <%= form.text_area :message, rows: 1, placeholder: random_chat_placeholder, disabled: local_assigns[:responding] || !local_assigns[:response_enabled], class: "lexi-textarea lexi-textarea-min-height", data: { action: "keypress.enter->meta-workflows--lexi-form-submit#handleKeyDown submit->meta-workflows--lexi-form-submit#handleSubmit", "meta-workflows--lexi-form-submit-target": "textarea" } %>
15
- </div>
15
+ <!-- Text Input -->
16
+ <div class="lexi-textarea-wrapper">
17
+ <%= form.text_area :message, rows: 1, placeholder: random_chat_placeholder, disabled: local_assigns[:responding] || !local_assigns[:response_enabled], class: "lexi-textarea lexi-textarea-min-height", data: { action: "keypress.enter->meta-workflows--lexi-form-submit#handleKeyDown submit->meta-workflows--lexi-form-submit#handleSubmit", "meta-workflows--lexi-form-submit-target": "textarea" } %>
18
+ </div>
16
19
 
17
- <div class="lexi-input-controls">
18
- <div class="lexi-input-icons">
19
- <button type="button" class="lexi-icon-button" disabled>
20
- <i class="fa-solid fa-microphone text-lg"></i>
21
- </button>
22
- <button type="button" class="lexi-icon-button" disabled>
23
- <i class="fa-solid fa-headphones text-lg"></i>
24
- </button>
25
- <button type="button" class="lexi-icon-button" disabled>
26
- <i class="fa-solid fa-waveform-lines text-lg"></i>
27
- </button>
28
- </div>
29
- <div>
30
- <!-- Send button -->
31
- <% if local_assigns[:responding] %>
32
- <button type="button" class="lexi-send-button responding" disabled>
33
- <i class="fa-solid fa-arrow-up text-lg"></i>
34
- </button>
35
- <% else %>
36
- <%= form.button type: "submit", class: "lexi-send-button sm-btn sm-btn-primary" do %>
37
- <i class="fa-solid fa-arrow-up text-lg"></i>
38
- <% end %>
39
- <% end %>
40
- </div>
41
- </div>
20
+ <div class="lexi-input-controls">
21
+ <div class="lexi-input-icons">
22
+ <button type="button" class="lexi-icon-button" disabled>
23
+ <i class="fa-solid fa-microphone text-lg"></i>
24
+ </button>
25
+ <button type="button" class="lexi-icon-button" disabled>
26
+ <i class="fa-solid fa-headphones text-lg"></i>
27
+ </button>
28
+ <button type="button" class="lexi-icon-button" disabled>
29
+ <i class="fa-solid fa-waveform-lines text-lg"></i>
30
+ </button>
31
+ </div>
32
+ <div>
33
+ <!-- Send button -->
34
+ <% if local_assigns[:responding] %>
35
+ <button type="button" class="lexi-send-button responding" disabled>
36
+ <i class="fa-solid fa-arrow-up text-lg"></i>
37
+ </button>
38
+ <% else %>
39
+ <%= form.button type: "submit", class: "lexi-send-button sm-btn sm-btn-primary" do %>
40
+ <i class="fa-solid fa-arrow-up text-lg"></i>
41
+ <% end %>
42
+ <% end %>
43
+ </div>
44
+ </div>
42
45
  </div>
43
46
 
44
47
  <p class="lexi-recording-notice">This chat is being recorded.</p>
@@ -48,7 +51,7 @@
48
51
  <!-- Empty placeholder to prevent layout jank -->
49
52
  <div aria-hidden="true"></div>
50
53
  <% else %>
51
- <%= form.button type: "submit", name: "advance", value: "true", class: "lexi-advance-button #{'opacity-50 cursor-not-allowed' if local_assigns[:responding]}", disabled: local_assigns[:responding] do %>
54
+ <%= form.button type: "submit", name: "manual_advance", value: "true", class: "lexi-advance-button #{'opacity-50 cursor-not-allowed' if local_assigns[:responding]}", disabled: local_assigns[:responding] do %>
52
55
  <i class="fa-light fa-arrow-right"></i> Next
53
56
  <% end %>
54
57
  <% end %>
@@ -0,0 +1,60 @@
1
+ <div class="structured-input-container"
2
+ role="group"
3
+ aria-labelledby="structured-slider-legend">
4
+ <legend id="structured-slider-legend" class="sr-only">Select a value using the slider</legend>
5
+ <div class="structured-slider-container">
6
+ <div class="structured-slider-labels">
7
+ <span class="structured-slider-label-min"><%= options['min']['label'] %></span>
8
+ <span class="structured-slider-label-max"><%= options['max']['label'] %></span>
9
+ </div>
10
+
11
+ <div class="structured-slider-wrapper">
12
+ <input
13
+ type="range"
14
+ id="range_value"
15
+ name="range_value"
16
+ min="<%= options['min']['value'] %>"
17
+ max="<%= options['max']['value'] %>"
18
+ step="<%= options['increment'] %>"
19
+ value="<%= options['min']['value'] %>"
20
+ class="structured-slider-input"
21
+ data-action="change->meta-workflows--structured-form-submit#handleSubmit"
22
+ aria-describedby="structured-slider-help structured-slider-value-display"
23
+ aria-valuemin="<%= options['min']['value'] %>"
24
+ aria-valuemax="<%= options['max']['value'] %>"
25
+ aria-valuenow="<%= options['min']['value'] %>"
26
+ aria-valuetext="<%= options['min']['value'] %>"
27
+ required
28
+ >
29
+ </div>
30
+
31
+ <div class="structured-slider-value">
32
+ <span class="structured-slider-current-value"
33
+ id="structured-slider-value-display"
34
+ data-range-display="<%= options['min']['value'] %>"
35
+ aria-live="polite">
36
+ <%= options['min']['value'] %>
37
+ </span>
38
+ </div>
39
+ <div id="structured-slider-help" class="sr-only">Use arrow keys to adjust the value</div>
40
+ </div>
41
+ </div>
42
+
43
+ <script>
44
+ // Update the displayed value when slider changes
45
+ document.addEventListener('DOMContentLoaded', function() {
46
+ const slider = document.getElementById('range_value');
47
+ const valueDisplay = document.querySelector('.structured-slider-current-value');
48
+
49
+ if (slider && valueDisplay) {
50
+ slider.addEventListener('input', function() {
51
+ valueDisplay.textContent = this.value;
52
+ valueDisplay.setAttribute('data-range-display', this.value);
53
+
54
+ // Update ARIA attributes for accessibility
55
+ slider.setAttribute('aria-valuenow', this.value);
56
+ slider.setAttribute('aria-valuetext', this.value);
57
+ });
58
+ }
59
+ });
60
+ </script>
@@ -0,0 +1,28 @@
1
+ <%= turbo_frame_tag target_frame_id(record, structured_input: true) do %>
2
+ <% if local_assigns[:is_structured_input] && structured_input_config.present? %>
3
+ <%
4
+ # Get workflow execution for form submission
5
+ workflow_execution = record.workflow_executions.order(created_at: :desc).first
6
+ form_url = workflow_execution ? meta_workflows.structured_human_path(workflow_execution.id) : "#"
7
+ chat_id = local_assigns[:chat_id] || workflow_execution&.workflow_steps&.last&.chat&.id
8
+ %>
9
+
10
+ <%= form_with url: form_url,
11
+ method: :patch,
12
+ id: "#{target_frame_id(record, structured_input: true)}_form",
13
+ data: { controller: "meta-workflows--structured-form-submit", "meta-workflows--structured-form-submit-target": "form" } do |form| %>
14
+ <%= form.hidden_field :chat_id, value: chat_id %>
15
+
16
+ <div class="structured-input-wrapper">
17
+ <% case structured_input_config['type'] %>
18
+ <% when 'single_choice' %>
19
+ <%= render 'meta_workflows/radio_input', options: structured_input_config['options'] %>
20
+ <% when 'multiple_choice' %>
21
+ <%= render 'meta_workflows/checkbox_input', options: structured_input_config['options'] %>
22
+ <% when 'range' %>
23
+ <%= render 'meta_workflows/slider_input', options: structured_input_config['options'] %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
27
+ <% end %>
28
+ <% end %>
data/config/routes.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  MetaWorkflows::Engine.routes.draw do
4
4
  resources :humans, only: [:update]
5
+ resources :structured_humans, only: [:update]
5
6
 
6
7
  # Workflow imports - RESTful routes
7
8
  resources :workflow_imports, only: %i[new create], path: 'workflow_imports' do
@@ -3,7 +3,7 @@
3
3
  module MetaWorkflows
4
4
  MAJOR = 0
5
5
  MINOR = 9
6
- PATCH = 19 # this is automatically incremented by the build process
6
+ PATCH = 20 # this is automatically incremented by the build process
7
7
 
8
8
  VERSION = "#{MetaWorkflows::MAJOR}.#{MetaWorkflows::MINOR}.#{MetaWorkflows::PATCH}".freeze
9
9
  end
@@ -45,6 +45,8 @@ module Services
45
45
  process_agent_action(conversation, workflow_execution, workflow)
46
46
  when 'human'
47
47
  process_human_action(execution_step, workflow_execution)
48
+ when 'structured_human'
49
+ process_structured_human_action(execution_step, workflow_execution)
48
50
  when 'record_redirect'
49
51
  process_record_redirect_action(workflow_execution)
50
52
  when 'collection_create'
@@ -133,6 +135,18 @@ module Services
133
135
  # the step advancement is handled in the human input job
134
136
  end
135
137
 
138
+ def process_structured_human_action(execution_step, workflow_execution)
139
+ ::MetaWorkflows::HumanInputJob.perform_later(
140
+ user_id: user&.id,
141
+ record: workflow_execution.record,
142
+ params: {
143
+ inputs: inputs,
144
+ prompt_id: execution_step['prompt_id']
145
+ }
146
+ )
147
+ # the step advancement is handled in the human input job
148
+ end
149
+
136
150
  def process_record_redirect_action(workflow_execution)
137
151
  ::MetaWorkflows::RecordRedirectJob.perform_later(workflow_execution: workflow_execution)
138
152
  workflow_execution.increment_step
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: meta_workflows
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.19
4
+ version: 0.9.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Medovyy
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-07-14 00:00:00.000000000 Z
12
+ date: 2025-07-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -125,6 +125,7 @@ files:
125
125
  - app/assets/javascripts/meta_workflows/controllers/onboarding_controller.js
126
126
  - app/assets/javascripts/meta_workflows/controllers/redirect_controller.js
127
127
  - app/assets/javascripts/meta_workflows/controllers/response_scroll_controller.js
128
+ - app/assets/javascripts/meta_workflows/controllers/structured_form_submit_controller.js
128
129
  - app/assets/javascripts/meta_workflows/controllers/tray_controller.js
129
130
  - app/assets/javascripts/meta_workflows_manifest.js
130
131
  - app/assets/stylesheets/meta_workflows/application.css
@@ -135,6 +136,7 @@ files:
135
136
  - app/controllers/meta_workflows/debug_controller.rb
136
137
  - app/controllers/meta_workflows/humans_controller.rb
137
138
  - app/controllers/meta_workflows/meta_controller.rb
139
+ - app/controllers/meta_workflows/structured_humans_controller.rb
138
140
  - app/controllers/meta_workflows/workflow_imports_controller.rb
139
141
  - app/helpers/meta_workflows/application_helper.rb
140
142
  - app/helpers/meta_workflows/debug_helper.rb
@@ -166,13 +168,17 @@ files:
166
168
  - app/sidekiq/meta_workflows/tools/meta_workflow_tool.rb
167
169
  - app/views/layouts/meta_workflows/application.html.erb
168
170
  - app/views/meta_workflows/_assistant_message_bubble.html.erb
171
+ - app/views/meta_workflows/_checkbox_input.html.erb
169
172
  - app/views/meta_workflows/_error_message.html.erb
170
173
  - app/views/meta_workflows/_lexi_chat_alpha_tray.html.erb
171
174
  - app/views/meta_workflows/_lexi_chat_right_tray.html.erb
172
175
  - app/views/meta_workflows/_loader_message.html.erb
176
+ - app/views/meta_workflows/_radio_input.html.erb
173
177
  - app/views/meta_workflows/_redirect.html.erb
174
178
  - app/views/meta_workflows/_response_form_lexi.html.erb
175
179
  - app/views/meta_workflows/_response_lexi.html.erb
180
+ - app/views/meta_workflows/_slider_input.html.erb
181
+ - app/views/meta_workflows/_structured_input.html.erb
176
182
  - app/views/meta_workflows/_user_message_bubble.html.erb
177
183
  - app/views/meta_workflows/debug/executions.html.erb
178
184
  - app/views/meta_workflows/debug/show_execution.html.erb