flowengine-rails 0.1.0

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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/branch-name.sh +49 -0
  3. data/.claude/commands/create-pr.md +93 -0
  4. data/.claude/commands/stash-unstaged.md +21 -0
  5. data/.claude/commands/unstash-unstaged.md +15 -0
  6. data/.claude/settings.json +72 -0
  7. data/.rubocop_todo.yml +17 -0
  8. data/.ruby-version +1 -0
  9. data/CHANGELOG.md +5 -0
  10. data/CLAUDE.md +153 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +294 -0
  13. data/Rakefile +43 -0
  14. data/app/assets/javascripts/flow_engine/embed.js +44 -0
  15. data/app/assets/javascripts/flow_engine/progress_controller.js +17 -0
  16. data/app/assets/javascripts/flow_engine/step_controller.js +22 -0
  17. data/app/assets/stylesheets/flow_engine/application.css +569 -0
  18. data/app/controllers/flow_engine/admin/definitions_controller.rb +95 -0
  19. data/app/controllers/flow_engine/application_controller.rb +41 -0
  20. data/app/controllers/flow_engine/sessions_controller.rb +91 -0
  21. data/app/helpers/flow_engine/sessions_helper.rb +20 -0
  22. data/app/models/flow_engine/application_record.rb +8 -0
  23. data/app/models/flow_engine/flow_definition.rb +75 -0
  24. data/app/models/flow_engine/flow_session.rb +98 -0
  25. data/app/views/flow_engine/admin/definitions/_form.html.erb +27 -0
  26. data/app/views/flow_engine/admin/definitions/edit.html.erb +5 -0
  27. data/app/views/flow_engine/admin/definitions/index.html.erb +41 -0
  28. data/app/views/flow_engine/admin/definitions/mermaid.html.erb +9 -0
  29. data/app/views/flow_engine/admin/definitions/new.html.erb +5 -0
  30. data/app/views/flow_engine/admin/definitions/show.html.erb +25 -0
  31. data/app/views/flow_engine/sessions/completed.html.erb +34 -0
  32. data/app/views/flow_engine/sessions/new.html.erb +17 -0
  33. data/app/views/flow_engine/sessions/show.html.erb +26 -0
  34. data/app/views/flow_engine/sessions/steps/_boolean.html.erb +10 -0
  35. data/app/views/flow_engine/sessions/steps/_display.html.erb +4 -0
  36. data/app/views/flow_engine/sessions/steps/_multi_select.html.erb +8 -0
  37. data/app/views/flow_engine/sessions/steps/_number.html.erb +3 -0
  38. data/app/views/flow_engine/sessions/steps/_number_matrix.html.erb +13 -0
  39. data/app/views/flow_engine/sessions/steps/_single_select.html.erb +8 -0
  40. data/app/views/flow_engine/sessions/steps/_text.html.erb +11 -0
  41. data/app/views/flow_engine/sessions/steps/_unknown.html.erb +4 -0
  42. data/app/views/layouts/flow_engine/application.html.erb +26 -0
  43. data/app/views/layouts/flow_engine/embed.html.erb +30 -0
  44. data/config/routes.rb +22 -0
  45. data/db/migrate/01_create_flow_engine_definitions.rb +18 -0
  46. data/db/migrate/02_create_flow_engine_sessions.rb +18 -0
  47. data/exe/flowengine-rails +4 -0
  48. data/justfile +49 -0
  49. data/lefthook.yml +16 -0
  50. data/lib/flowengine/rails/configuration.rb +23 -0
  51. data/lib/flowengine/rails/dsl_loader.rb +35 -0
  52. data/lib/flowengine/rails/engine.rb +26 -0
  53. data/lib/flowengine/rails/version.rb +7 -0
  54. data/lib/flowengine/rails.rb +27 -0
  55. data/lib/generators/flow_engine/flow/flow_generator.rb +29 -0
  56. data/lib/generators/flow_engine/flow/templates/flow_definition.rb.tt +27 -0
  57. data/lib/generators/flow_engine/flow/templates/seed_task.rake.tt +22 -0
  58. data/lib/generators/flow_engine/install/install_generator.rb +34 -0
  59. data/lib/generators/flow_engine/install/templates/initializer.rb +25 -0
  60. data/log/.gitkeep +0 -0
  61. data/sig/flowengine/rails.rbs +6 -0
  62. metadata +164 -0
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ class SessionsController < ApplicationController
5
+ before_action :set_session, only: %i[show update completed abandon]
6
+
7
+ def new
8
+ @definitions = FlowEngine::FlowDefinition.active.order(:name)
9
+ @definition_id = params[:definition_id]
10
+ end
11
+
12
+ def create
13
+ definition = FlowEngine::FlowDefinition.find(params[:definition_id])
14
+ engine = FlowEngine::Engine.new(definition.parsed_definition)
15
+ state = engine.to_state
16
+
17
+ session = FlowEngine::FlowSession.create!(
18
+ flow_definition: definition,
19
+ current_step_id: state[:current_step_id]&.to_s,
20
+ answers: {},
21
+ history: state[:history].map(&:to_s),
22
+ status: "in_progress"
23
+ )
24
+
25
+ redirect_to session_path(session, embed_params)
26
+ end
27
+
28
+ def show
29
+ if @flow_session.completed?
30
+ redirect_to completed_session_path(@flow_session, embed_params)
31
+ return
32
+ end
33
+
34
+ @node = @flow_session.current_node
35
+ @progress = @flow_session.progress_percentage
36
+ end
37
+
38
+ def update
39
+ unless @flow_session.in_progress?
40
+ redirect_to session_path(@flow_session, embed_params)
41
+ return
42
+ end
43
+
44
+ answer_value = parse_answer
45
+ @flow_session.advance!(answer_value)
46
+
47
+ if @flow_session.completed?
48
+ redirect_to completed_session_path(@flow_session, embed_params)
49
+ else
50
+ redirect_to session_path(@flow_session, embed_params)
51
+ end
52
+ end
53
+
54
+ def completed
55
+ @result = @flow_session.result_json
56
+ end
57
+
58
+ def abandon
59
+ @flow_session.abandon!
60
+ redirect_to new_session_path(embed_params), notice: "Session abandoned."
61
+ end
62
+
63
+ private
64
+
65
+ def set_session
66
+ @flow_session = FlowEngine::FlowSession.find(params[:id])
67
+ end
68
+
69
+ def embed_params
70
+ embed_mode? ? { embed: "true" } : {}
71
+ end
72
+
73
+ def parse_answer
74
+ node = @flow_session.current_node
75
+ return params[:answer] unless node
76
+
77
+ case node.type
78
+ when :multi_select
79
+ params[:answer_values] || []
80
+ when :number_matrix
81
+ (params[:answer_fields] || {}).to_unsafe_h.transform_values(&:to_i)
82
+ when :number
83
+ params[:answer].to_i
84
+ when :boolean
85
+ params[:answer] == "true"
86
+ else
87
+ params[:answer]
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ module SessionsHelper
5
+ STEP_PARTIALS = {
6
+ multi_select: "flow_engine/sessions/steps/multi_select",
7
+ single_select: "flow_engine/sessions/steps/single_select",
8
+ number_matrix: "flow_engine/sessions/steps/number_matrix",
9
+ text: "flow_engine/sessions/steps/text",
10
+ number: "flow_engine/sessions/steps/number",
11
+ boolean: "flow_engine/sessions/steps/boolean",
12
+ display: "flow_engine/sessions/steps/display"
13
+ }.freeze
14
+
15
+ def render_step(node, form)
16
+ partial = STEP_PARTIALS.fetch(node.type, "flow_engine/sessions/steps/unknown")
17
+ render partial: partial, locals: { node: node, form: form }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ self.table_name_prefix = "flow_engine_"
7
+ end
8
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ class FlowDefinition < ApplicationRecord
5
+ self.table_name = "flow_engine_definitions"
6
+
7
+ has_many :flow_sessions, dependent: :restrict_with_error, foreign_key: :definition_id, inverse_of: :flow_definition
8
+
9
+ validates :name, presence: true
10
+ validates :version, presence: true,
11
+ numericality: { only_integer: true, greater_than: 0 },
12
+ uniqueness: { scope: :name }
13
+ validates :dsl, presence: true
14
+ validate :dsl_must_parse
15
+
16
+ attr_accessor :skip_auto_version
17
+
18
+ before_validation :auto_increment_version, on: :create, unless: :skip_auto_version
19
+
20
+ scope :active, -> { where(active: true) }
21
+ scope :by_name, ->(name) { where(name: name) }
22
+ scope :latest_version, ->(name) { by_name(name).order(version: :desc).first }
23
+
24
+ def parsed_definition
25
+ @parsed_definition ||= FlowEngine::Rails::DslLoader.load(dsl, cache_key: cache_key_for_dsl)
26
+ end
27
+
28
+ def activate!
29
+ transaction do
30
+ self.class.where(name: name, active: true).where.not(id: id).update_all(active: false)
31
+ update!(active: true)
32
+ end
33
+ end
34
+
35
+ def deactivate!
36
+ update!(active: false)
37
+ end
38
+
39
+ def readonly?
40
+ flow_sessions.exists? && persisted? && !new_record?
41
+ end
42
+
43
+ def mermaid_diagram
44
+ exporter = FlowEngine::Graph::MermaidExporter.new(parsed_definition)
45
+ exporter.export
46
+ end
47
+
48
+ def step_ids
49
+ parsed_definition.step_ids
50
+ end
51
+
52
+ def total_steps
53
+ step_ids.size
54
+ end
55
+
56
+ private
57
+
58
+ def cache_key_for_dsl
59
+ "definition:#{id}:v#{version}" if persisted?
60
+ end
61
+
62
+ def auto_increment_version
63
+ max_version = self.class.where(name: name).maximum(:version) || 0
64
+ self.version = max_version + 1
65
+ end
66
+
67
+ def dsl_must_parse
68
+ return if dsl.blank?
69
+
70
+ FlowEngine::Rails::DslLoader.load(dsl)
71
+ rescue FlowEngine::Errors::Error => e
72
+ errors.add(:dsl, "is invalid: #{e.message}")
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ class FlowSession < ApplicationRecord
5
+ self.table_name = "flow_engine_sessions"
6
+
7
+ belongs_to :flow_definition, foreign_key: :definition_id, inverse_of: :flow_sessions
8
+
9
+ validates :status, presence: true, inclusion: { in: %w[in_progress completed abandoned] }
10
+
11
+ scope :in_progress, -> { where(status: "in_progress") }
12
+ scope :completed, -> { where(status: "completed") }
13
+ scope :abandoned, -> { where(status: "abandoned") }
14
+
15
+ def engine
16
+ state = {
17
+ current_step_id: current_step_id,
18
+ answers: answers || {},
19
+ history: history || []
20
+ }
21
+ FlowEngine::Engine.from_state(flow_definition.parsed_definition, state)
22
+ end
23
+
24
+ def advance!(answer_value)
25
+ eng = engine
26
+ eng.answer(answer_value)
27
+ persist_engine_state!(eng)
28
+ end
29
+
30
+ def abandon!
31
+ update!(status: "abandoned")
32
+ end
33
+
34
+ def in_progress?
35
+ status == "in_progress"
36
+ end
37
+
38
+ def completed?
39
+ status == "completed"
40
+ end
41
+
42
+ def abandoned?
43
+ status == "abandoned"
44
+ end
45
+
46
+ def finished?
47
+ completed? || abandoned?
48
+ end
49
+
50
+ def current_node
51
+ return nil if current_step_id.blank?
52
+
53
+ flow_definition.parsed_definition.step(current_step_id.to_sym)
54
+ rescue FlowEngine::UnknownStepError
55
+ nil
56
+ end
57
+
58
+ def progress_percentage
59
+ total = flow_definition.total_steps
60
+ return 0 if total.zero?
61
+
62
+ completed_count = (history || []).size
63
+ [(completed_count.to_f / total * 100).round, 100].min
64
+ end
65
+
66
+ def result_json
67
+ {
68
+ definition_name: flow_definition.name,
69
+ definition_version: flow_definition.version,
70
+ answers: answers,
71
+ history: history,
72
+ status: status,
73
+ completed_at: updated_at&.iso8601
74
+ }
75
+ end
76
+
77
+ private
78
+
79
+ def persist_engine_state!(eng)
80
+ state = eng.to_state
81
+ new_status = eng.finished? ? "completed" : "in_progress"
82
+
83
+ update!(
84
+ current_step_id: state[:current_step_id]&.to_s,
85
+ answers: state[:answers].transform_keys(&:to_s),
86
+ history: state[:history].map(&:to_s),
87
+ status: new_status
88
+ )
89
+
90
+ fire_completion_callback if new_status == "completed"
91
+ end
92
+
93
+ def fire_completion_callback
94
+ callback = FlowEngine::Rails.configuration.on_session_complete
95
+ callback&.call(self)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,27 @@
1
+ <%= form_with model: [:admin, definition], url: url, method: method, local: true do |form| %>
2
+ <% if definition.errors.any? %>
3
+ <div class="fe-errors">
4
+ <h3><%= pluralize(definition.errors.count, "error") %> prevented this definition from being saved:</h3>
5
+ <ul>
6
+ <% definition.errors.full_messages.each do |msg| %>
7
+ <li><%= msg %></li>
8
+ <% end %>
9
+ </ul>
10
+ </div>
11
+ <% end %>
12
+
13
+ <div class="fe-form-group">
14
+ <%= form.label :name, class: "fe-label" %>
15
+ <%= form.text_field :name, class: "fe-input" %>
16
+ </div>
17
+
18
+ <div class="fe-form-group">
19
+ <%= form.label :dsl, "DSL Definition", class: "fe-label" %>
20
+ <%= form.text_area :dsl, rows: 20, class: "fe-input fe-input--textarea fe-input--code" %>
21
+ </div>
22
+
23
+ <div class="fe-form-group">
24
+ <%= form.submit class: "fe-btn fe-btn--primary" %>
25
+ <%= link_to "Cancel", flow_engine.admin_definitions_path, class: "fe-btn" %>
26
+ </div>
27
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <h1>Edit: <%= @definition.name %> v<%= @definition.version %></h1>
2
+
3
+ <%= render "form", definition: @definition,
4
+ url: flow_engine.admin_definition_path(@definition),
5
+ method: :patch %>
@@ -0,0 +1,41 @@
1
+ <h1>Flow Definitions</h1>
2
+
3
+ <%= link_to "New Definition", flow_engine.new_admin_definition_path, class: "fe-btn fe-btn--primary" %>
4
+
5
+ <% @grouped.each do |name, versions| %>
6
+ <div class="fe-definition-group">
7
+ <h2><%= name %></h2>
8
+ <table class="fe-table">
9
+ <thead>
10
+ <tr>
11
+ <th>Version</th>
12
+ <th>Active</th>
13
+ <th>Sessions</th>
14
+ <th>Created</th>
15
+ <th>Actions</th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% versions.each do |definition| %>
20
+ <tr>
21
+ <td>v<%= definition.version %></td>
22
+ <td><%= definition.active? ? "Yes" : "No" %></td>
23
+ <td><%= definition.flow_sessions.count %></td>
24
+ <td><%= definition.created_at.strftime("%Y-%m-%d %H:%M") %></td>
25
+ <td>
26
+ <%= link_to "View", flow_engine.admin_definition_path(definition) %>
27
+ <% unless definition.readonly? %>
28
+ | <%= link_to "Edit", flow_engine.edit_admin_definition_path(definition) %>
29
+ <% end %>
30
+ <% if definition.active? %>
31
+ | <%= link_to "Deactivate", flow_engine.deactivate_admin_definition_path(definition), data: { turbo_method: :post } %>
32
+ <% else %>
33
+ | <%= link_to "Activate", flow_engine.activate_admin_definition_path(definition), data: { turbo_method: :post } %>
34
+ <% end %>
35
+ </td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <h1>Mermaid Diagram: <%= @definition.name %> v<%= @definition.version %></h1>
2
+
3
+ <div class="fe-mermaid">
4
+ <pre class="fe-code"><code><%= @diagram %></code></pre>
5
+ </div>
6
+
7
+ <p>
8
+ <%= link_to "Back", flow_engine.admin_definition_path(@definition), class: "fe-btn" %>
9
+ </p>
@@ -0,0 +1,5 @@
1
+ <h1>New Flow Definition</h1>
2
+
3
+ <%= render "form", definition: @definition,
4
+ url: flow_engine.admin_definitions_path,
5
+ method: :post %>
@@ -0,0 +1,25 @@
1
+ <h1><%= @definition.name %> <small>v<%= @definition.version %></small></h1>
2
+
3
+ <div class="fe-definition-detail">
4
+ <p><strong>Status:</strong> <%= @definition.active? ? "Active" : "Inactive" %></p>
5
+ <p><strong>Sessions:</strong> <%= @definition.flow_sessions.count %></p>
6
+ <p><strong>Steps:</strong> <%= @definition.total_steps %></p>
7
+
8
+ <div class="fe-definition-detail__actions">
9
+ <% if @definition.active? %>
10
+ <%= link_to "Deactivate", flow_engine.deactivate_admin_definition_path(@definition), method: :post, data: { turbo_method: :post }, class: "fe-btn" %>
11
+ <% else %>
12
+ <%= link_to "Activate", flow_engine.activate_admin_definition_path(@definition), method: :post, data: { turbo_method: :post }, class: "fe-btn fe-btn--primary" %>
13
+ <% end %>
14
+
15
+ <% unless @definition.readonly? %>
16
+ <%= link_to "Edit", flow_engine.edit_admin_definition_path(@definition), class: "fe-btn" %>
17
+ <% end %>
18
+
19
+ <%= link_to "Mermaid Diagram", flow_engine.mermaid_admin_definition_path(@definition), class: "fe-btn" %>
20
+ <%= link_to "Back", flow_engine.admin_definitions_path, class: "fe-btn" %>
21
+ </div>
22
+
23
+ <h2>DSL Source</h2>
24
+ <pre class="fe-code"><code><%= @definition.dsl %></code></pre>
25
+ </div>
@@ -0,0 +1,34 @@
1
+ <div class="fe-completed">
2
+ <h1>Flow Complete</h1>
3
+
4
+ <div class="fe-completed__summary">
5
+ <h2><%= @flow_session.flow_definition.name %> — Results</h2>
6
+
7
+ <table class="fe-table">
8
+ <thead>
9
+ <tr>
10
+ <th>Step</th>
11
+ <th>Answer</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <% (@flow_session.answers || {}).each do |step_id, answer| %>
16
+ <tr>
17
+ <td><%= step_id %></td>
18
+ <td><%= answer.is_a?(Hash) || answer.is_a?(Array) ? answer.to_json : answer %></td>
19
+ </tr>
20
+ <% end %>
21
+ </tbody>
22
+ </table>
23
+ </div>
24
+
25
+ <% if embed_mode? %>
26
+ <script>
27
+ window.parent.postMessage({
28
+ type: 'flowengine:completed',
29
+ sessionId: '<%= @flow_session.id %>',
30
+ answers: <%= raw @result.to_json %>
31
+ }, '*');
32
+ </script>
33
+ <% end %>
34
+ </div>
@@ -0,0 +1,17 @@
1
+ <h1>Start a New Flow</h1>
2
+
3
+ <% if @definitions.any? %>
4
+ <div class="fe-definition-list">
5
+ <% @definitions.each do |definition| %>
6
+ <div class="fe-definition-card">
7
+ <h2><%= definition.name %> <small>v<%= definition.version %></small></h2>
8
+ <%= link_to "Start",
9
+ flow_engine.sessions_path(definition_id: definition.id, **(@embed_mode ? { embed: "true" } : {})),
10
+ class: "fe-btn fe-btn--primary",
11
+ data: { turbo_method: :post } %>
12
+ </div>
13
+ <% end %>
14
+ </div>
15
+ <% else %>
16
+ <p>No active flow definitions available.</p>
17
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <div class="fe-session" data-controller="step">
2
+ <div class="fe-progress" data-controller="progress" data-progress-value="<%= @progress %>">
3
+ <div class="fe-progress__bar" style="width: <%= @progress %>%"></div>
4
+ </div>
5
+ <span class="fe-progress__text">Step <%= @progress %>% complete</span>
6
+
7
+ <% if @node %>
8
+ <div class="fe-step">
9
+ <h2 class="fe-step__question"><%= @node.question %></h2>
10
+
11
+ <%= form_with url: flow_engine.session_path(@flow_session, **embed_mode? ? { embed: "true" } : {}),
12
+ method: :patch,
13
+ local: true,
14
+ data: { step_target: "form" } do |form| %>
15
+
16
+ <%= render_step(@node, form) %>
17
+
18
+ <div class="fe-step__actions">
19
+ <%= form.submit "Continue", class: "fe-btn fe-btn--primary", data: { step_target: "submit" } %>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+ <% else %>
24
+ <p>This session has no current step.</p>
25
+ <% end %>
26
+ </div>
@@ -0,0 +1,10 @@
1
+ <div class="fe-field fe-field--boolean">
2
+ <label class="fe-radio">
3
+ <input type="radio" name="answer" value="true">
4
+ <span>Yes</span>
5
+ </label>
6
+ <label class="fe-radio">
7
+ <input type="radio" name="answer" value="false">
8
+ <span>No</span>
9
+ </label>
10
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="fe-field fe-field--display">
2
+ <p class="fe-display-text"><%= node.question %></p>
3
+ <input type="hidden" name="answer" value="acknowledged">
4
+ </div>
@@ -0,0 +1,8 @@
1
+ <div class="fe-field fe-field--multi-select">
2
+ <% (node.options || []).each do |option| %>
3
+ <label class="fe-checkbox">
4
+ <input type="checkbox" name="answer_values[]" value="<%= option %>">
5
+ <span><%= option %></span>
6
+ </label>
7
+ <% end %>
8
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="fe-field fe-field--number">
2
+ <input type="number" name="answer" value="0" min="0" class="fe-input fe-input--number">
3
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="fe-field fe-field--number-matrix">
2
+ <% (node.fields || []).each do |field_name| %>
3
+ <div class="fe-matrix-row">
4
+ <label class="fe-matrix-row__label" for="answer_fields_<%= field_name %>"><%= field_name %></label>
5
+ <input type="number"
6
+ name="answer_fields[<%= field_name %>]"
7
+ id="answer_fields_<%= field_name %>"
8
+ value="0"
9
+ min="0"
10
+ class="fe-input fe-input--number">
11
+ </div>
12
+ <% end %>
13
+ </div>
@@ -0,0 +1,8 @@
1
+ <div class="fe-field fe-field--single-select">
2
+ <% (node.options || []).each do |option| %>
3
+ <label class="fe-radio">
4
+ <input type="radio" name="answer" value="<%= option %>">
5
+ <span><%= option %></span>
6
+ </label>
7
+ <% end %>
8
+ </div>
@@ -0,0 +1,11 @@
1
+ <div class="fe-field fe-field--text">
2
+ <textarea name="answer"
3
+ rows="6"
4
+ class="fe-input fe-input--textarea"
5
+ placeholder="<%= node.decorations&.dig(:placeholder) || "Type your answer here..." %>"
6
+ <% if node.decorations&.dig(:maxlength) %>maxlength="<%= node.decorations[:maxlength] %>"<% end %>
7
+ ></textarea>
8
+ <% if node.decorations&.dig(:hint) %>
9
+ <p class="fe-field__hint"><%= node.decorations[:hint] %></p>
10
+ <% end %>
11
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="fe-field fe-field--unknown">
2
+ <p class="fe-field__hint">Unknown step type: <%= node.type %></p>
3
+ <textarea name="answer" rows="4" class="fe-input fe-input--textarea" placeholder="Enter your answer..."></textarea>
4
+ </div>
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>FlowEngine</title>
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap">
12
+ <link rel="stylesheet" href="<%= flow_engine.root_path %>assets/flow_engine/application.css">
13
+ </head>
14
+ <body>
15
+ <div class="fe-container">
16
+ <% if flash[:notice] %>
17
+ <div class="fe-flash fe-flash--notice"><%= flash[:notice] %></div>
18
+ <% end %>
19
+ <% if flash[:alert] %>
20
+ <div class="fe-flash fe-flash--alert"><%= flash[:alert] %></div>
21
+ <% end %>
22
+
23
+ <%= yield %>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap">
10
+ <link rel="stylesheet" href="<%= flow_engine.root_path %>assets/flow_engine/application.css">
11
+ <style>
12
+ body { margin: 0; padding: 0; background: transparent; }
13
+ .fe-container { --fe-max-width: 100%; padding: var(--fe-space-md); }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div class="fe-container">
18
+ <%= yield %>
19
+ </div>
20
+
21
+ <script>
22
+ function postResize() {
23
+ var height = document.body.scrollHeight;
24
+ window.parent.postMessage({ type: 'flowengine:resize', height: height }, '*');
25
+ }
26
+ window.addEventListener('load', postResize);
27
+ new MutationObserver(postResize).observe(document.body, { childList: true, subtree: true });
28
+ </script>
29
+ </body>
30
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ FlowEngine::Rails::Engine.routes.draw do
4
+ resources :sessions, only: %i[new create show update] do
5
+ member do
6
+ get :completed
7
+ patch :abandon
8
+ end
9
+ end
10
+
11
+ namespace :admin do
12
+ resources :definitions do
13
+ member do
14
+ post :activate
15
+ post :deactivate
16
+ get :mermaid
17
+ end
18
+ end
19
+ end
20
+
21
+ root to: "sessions#new"
22
+ end