active_canvas 0.0.1

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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +318 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/active_canvas/editor/ai_panel.js +1607 -0
  6. data/app/assets/javascripts/active_canvas/editor/asset_manager.js +498 -0
  7. data/app/assets/javascripts/active_canvas/editor/blocks.js +1083 -0
  8. data/app/assets/javascripts/active_canvas/editor/code_panel.js +572 -0
  9. data/app/assets/javascripts/active_canvas/editor/component_toolbar.js +394 -0
  10. data/app/assets/javascripts/active_canvas/editor/panels.js +460 -0
  11. data/app/assets/javascripts/active_canvas/editor/utils.js +56 -0
  12. data/app/assets/javascripts/active_canvas/editor.js +295 -0
  13. data/app/assets/stylesheets/active_canvas/application.css +15 -0
  14. data/app/assets/stylesheets/active_canvas/editor.css +2929 -0
  15. data/app/controllers/active_canvas/admin/ai_controller.rb +181 -0
  16. data/app/controllers/active_canvas/admin/application_controller.rb +56 -0
  17. data/app/controllers/active_canvas/admin/media_controller.rb +61 -0
  18. data/app/controllers/active_canvas/admin/page_types_controller.rb +57 -0
  19. data/app/controllers/active_canvas/admin/page_versions_controller.rb +23 -0
  20. data/app/controllers/active_canvas/admin/pages_controller.rb +133 -0
  21. data/app/controllers/active_canvas/admin/partials_controller.rb +88 -0
  22. data/app/controllers/active_canvas/admin/settings_controller.rb +256 -0
  23. data/app/controllers/active_canvas/application_controller.rb +20 -0
  24. data/app/controllers/active_canvas/pages_controller.rb +18 -0
  25. data/app/controllers/concerns/active_canvas/current_user.rb +12 -0
  26. data/app/controllers/concerns/active_canvas/rate_limitable.rb +75 -0
  27. data/app/controllers/concerns/active_canvas/tailwind_compilation.rb +39 -0
  28. data/app/helpers/active_canvas/application_helper.rb +4 -0
  29. data/app/jobs/active_canvas/application_job.rb +4 -0
  30. data/app/jobs/active_canvas/compile_tailwind_job.rb +64 -0
  31. data/app/mailers/active_canvas/application_mailer.rb +6 -0
  32. data/app/models/active_canvas/ai_model.rb +136 -0
  33. data/app/models/active_canvas/application_record.rb +5 -0
  34. data/app/models/active_canvas/media.rb +141 -0
  35. data/app/models/active_canvas/page.rb +85 -0
  36. data/app/models/active_canvas/page_type.rb +22 -0
  37. data/app/models/active_canvas/page_version.rb +80 -0
  38. data/app/models/active_canvas/partial.rb +73 -0
  39. data/app/models/active_canvas/setting.rb +292 -0
  40. data/app/services/active_canvas/ai_configuration.rb +40 -0
  41. data/app/services/active_canvas/ai_models.rb +128 -0
  42. data/app/services/active_canvas/ai_service.rb +289 -0
  43. data/app/services/active_canvas/content_sanitizer.rb +112 -0
  44. data/app/services/active_canvas/tailwind_compiler.rb +156 -0
  45. data/app/views/active_canvas/admin/media/index.html.erb +401 -0
  46. data/app/views/active_canvas/admin/media/show.html.erb +297 -0
  47. data/app/views/active_canvas/admin/page_types/_form.html.erb +25 -0
  48. data/app/views/active_canvas/admin/page_types/edit.html.erb +13 -0
  49. data/app/views/active_canvas/admin/page_types/index.html.erb +29 -0
  50. data/app/views/active_canvas/admin/page_types/new.html.erb +9 -0
  51. data/app/views/active_canvas/admin/page_types/show.html.erb +18 -0
  52. data/app/views/active_canvas/admin/page_versions/show.html.erb +469 -0
  53. data/app/views/active_canvas/admin/pages/_form.html.erb +62 -0
  54. data/app/views/active_canvas/admin/pages/content.html.erb +139 -0
  55. data/app/views/active_canvas/admin/pages/edit.html.erb +335 -0
  56. data/app/views/active_canvas/admin/pages/editor.html.erb +710 -0
  57. data/app/views/active_canvas/admin/pages/index.html.erb +149 -0
  58. data/app/views/active_canvas/admin/pages/new.html.erb +19 -0
  59. data/app/views/active_canvas/admin/pages/show.html.erb +258 -0
  60. data/app/views/active_canvas/admin/pages/versions.html.erb +333 -0
  61. data/app/views/active_canvas/admin/partials/edit.html.erb +182 -0
  62. data/app/views/active_canvas/admin/partials/editor.html.erb +703 -0
  63. data/app/views/active_canvas/admin/partials/index.html.erb +131 -0
  64. data/app/views/active_canvas/admin/settings/show.html.erb +1864 -0
  65. data/app/views/active_canvas/pages/no_homepage.html.erb +45 -0
  66. data/app/views/active_canvas/pages/show.html.erb +113 -0
  67. data/app/views/layouts/active_canvas/admin/application.html.erb +960 -0
  68. data/app/views/layouts/active_canvas/admin/editor.html.erb +826 -0
  69. data/app/views/layouts/active_canvas/application.html.erb +55 -0
  70. data/config/routes.rb +48 -0
  71. data/db/migrate/20260202000001_create_active_canvas_tables.rb +113 -0
  72. data/db/migrate/20260202000002_create_active_canvas_ai_models.rb +26 -0
  73. data/lib/active_canvas/configuration.rb +232 -0
  74. data/lib/active_canvas/engine.rb +44 -0
  75. data/lib/active_canvas/version.rb +3 -0
  76. data/lib/active_canvas.rb +26 -0
  77. data/lib/generators/active_canvas/install/install_generator.rb +263 -0
  78. data/lib/generators/active_canvas/install/templates/initializer.rb.tt +163 -0
  79. data/lib/tasks/active_canvas_tasks.rake +69 -0
  80. metadata +150 -0
@@ -0,0 +1,181 @@
1
+ module ActiveCanvas
2
+ module Admin
3
+ class AiController < ApplicationController
4
+ include ActionController::Live
5
+ include ActiveCanvas::RateLimitable
6
+
7
+ # Use null_session for JSON requests (allows CSRF token via header)
8
+ protect_from_forgery with: :null_session, if: -> { request.format.json? }
9
+
10
+ before_action :ensure_ai_configured, except: %i[status models]
11
+ before_action :verify_request_origin, only: %i[chat]
12
+ before_action :check_ai_rate_limit, only: %i[chat image screenshot_to_code]
13
+
14
+ def chat
15
+ unless AiConfiguration.text_enabled?
16
+ return render json: { error: "Text generation is disabled" }, status: :forbidden
17
+ end
18
+
19
+ response.headers["Content-Type"] = "text/event-stream"
20
+ response.headers["Cache-Control"] = "no-cache"
21
+ response.headers["X-Accel-Buffering"] = "no"
22
+ response.headers["Connection"] = "keep-alive"
23
+
24
+ config = ActiveCanvas.config
25
+ start_time = Time.current
26
+ last_write = Time.current
27
+ total_bytes = 0
28
+
29
+ begin
30
+ AiService.generate_text(
31
+ prompt: params[:prompt],
32
+ model: params[:model],
33
+ context: build_context
34
+ ) do |chunk|
35
+ # Check stream timeout
36
+ if Time.current - start_time > config.ai_stream_timeout
37
+ write_sse_event("error", { error: "Stream timeout exceeded" })
38
+ break
39
+ end
40
+
41
+ # Check idle timeout
42
+ if Time.current - last_write > config.ai_stream_idle_timeout
43
+ write_sse_event("error", { error: "Idle timeout - no data received" })
44
+ break
45
+ end
46
+
47
+ # Check response size limit
48
+ chunk_content = chunk.content.to_s
49
+ chunk_size = chunk_content.bytesize
50
+ total_bytes += chunk_size
51
+
52
+ if total_bytes > config.ai_max_response_size
53
+ write_sse_event("error", { error: "Response too large" })
54
+ break
55
+ end
56
+
57
+ write_sse_event("chunk", { content: chunk_content })
58
+ last_write = Time.current
59
+ end
60
+
61
+ write_sse_event("done", { total_bytes: total_bytes })
62
+ rescue IOError => e
63
+ # Client disconnected, this is normal
64
+ Rails.logger.debug "AI Chat client disconnected: #{e.message}"
65
+ rescue => e
66
+ Rails.logger.error "AI Chat Error: #{e.class.name}: #{e.message}"
67
+ begin
68
+ write_sse_event("error", { error: e.message })
69
+ rescue IOError
70
+ # Client already disconnected
71
+ end
72
+ ensure
73
+ response.stream.close rescue nil
74
+ end
75
+ end
76
+
77
+ def image
78
+ unless AiConfiguration.image_enabled?
79
+ return render json: { error: "Image generation is disabled" }, status: :forbidden
80
+ end
81
+
82
+ media = AiService.generate_image(
83
+ prompt: params[:prompt],
84
+ model: params[:model]
85
+ )
86
+
87
+ render json: {
88
+ success: true,
89
+ image: media.as_json_for_editor,
90
+ url: media.url
91
+ }
92
+ rescue => e
93
+ Rails.logger.error "AI Image Error: #{e.message}"
94
+ render json: { error: e.message }, status: :unprocessable_entity
95
+ end
96
+
97
+ def screenshot_to_code
98
+ unless AiConfiguration.screenshot_enabled?
99
+ return render json: { error: "Screenshot to code is disabled" }, status: :forbidden
100
+ end
101
+
102
+ unless params[:screenshot].present?
103
+ return render json: { error: "No screenshot provided" }, status: :bad_request
104
+ end
105
+
106
+ html = AiService.screenshot_to_code(
107
+ image_data: params[:screenshot],
108
+ model: params[:model],
109
+ additional_prompt: params[:additional_prompt]
110
+ )
111
+
112
+ render json: { success: true, html: html }
113
+ rescue => e
114
+ Rails.logger.error "AI Screenshot Error: #{e.message}"
115
+ render json: { error: e.message }, status: :unprocessable_entity
116
+ end
117
+
118
+ def models
119
+ text = AiModels.text_models
120
+ image = AiModels.image_models
121
+ vision = AiModels.vision_models
122
+
123
+ unless params[:debug]
124
+ strip = ->(list) { list.map { |m| m.except(:input_modalities, :output_modalities, "input_modalities", "output_modalities") } }
125
+ text = strip.call(text)
126
+ image = strip.call(image)
127
+ vision = strip.call(vision)
128
+ end
129
+
130
+ render json: {
131
+ text: text,
132
+ image: image,
133
+ vision: vision,
134
+ default_text: Setting.ai_default_text_model,
135
+ default_image: Setting.ai_default_image_model,
136
+ default_vision: Setting.ai_default_vision_model
137
+ }
138
+ end
139
+
140
+ def status
141
+ render json: {
142
+ configured: AiConfiguration.configured?,
143
+ providers: AiConfiguration.configured_providers,
144
+ text_enabled: AiConfiguration.text_enabled?,
145
+ image_enabled: AiConfiguration.image_enabled?,
146
+ screenshot_enabled: AiConfiguration.screenshot_enabled?
147
+ }
148
+ end
149
+
150
+ private
151
+
152
+ def ensure_ai_configured
153
+ unless AiConfiguration.configured?
154
+ render json: {
155
+ error: "AI not configured. Add API keys in Settings > AI."
156
+ }, status: :service_unavailable
157
+ end
158
+ end
159
+
160
+ def build_context
161
+ context = ""
162
+
163
+ if params[:mode] == "element" && params[:current_html].present?
164
+ context = "You are modifying an existing element. Here is the current HTML:\n```html\n#{params[:current_html]}\n```\n\nModify or enhance this element based on the user's request."
165
+ elsif params[:mode] == "page"
166
+ context = "Generate a complete page section or component that can be inserted into a page."
167
+ end
168
+
169
+ context
170
+ end
171
+
172
+ def write_sse_event(event, data)
173
+ response.stream.write("event: #{event}\ndata: #{data.to_json}\n\n")
174
+ end
175
+
176
+ def check_ai_rate_limit
177
+ check_rate_limit(namespace: "ai")
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveCanvas
2
+ module Admin
3
+ class ApplicationController < ActiveCanvas.config.admin_parent_controller.constantize
4
+ include ActiveCanvas::CurrentUser
5
+
6
+ protect_from_forgery with: :exception
7
+ layout "active_canvas/admin/application"
8
+
9
+ before_action :enforce_authentication_configured
10
+ before_action :active_canvas_authenticate_admin
11
+
12
+ private
13
+
14
+ def enforce_authentication_configured
15
+ # Skip if inheriting from a custom parent controller (assumes parent handles auth)
16
+ return if ActiveCanvas.config.admin_parent_controller != "ActionController::Base"
17
+
18
+ ActiveCanvas.config.enforce_authentication!
19
+ rescue SecurityError => e
20
+ Rails.logger.error(e.message)
21
+ render plain: "Admin authentication not configured. Check server logs.", status: :forbidden
22
+ end
23
+
24
+ def active_canvas_authenticate_admin
25
+ # Skip if inheriting from a custom parent controller (assumes parent handles auth)
26
+ return if ActiveCanvas.config.admin_parent_controller != "ActionController::Base"
27
+
28
+ auth = ActiveCanvas.config.authenticate_admin
29
+ return unless auth
30
+
31
+ if auth == :http_basic_auth
32
+ authenticate_with_http_basic_auth
33
+ elsif auth.is_a?(Symbol)
34
+ send(auth)
35
+ elsif auth.respond_to?(:call)
36
+ instance_exec(&auth)
37
+ end
38
+ end
39
+
40
+ def authenticate_with_http_basic_auth
41
+ config = ActiveCanvas.config
42
+
43
+ unless config.http_basic_auth_configured?
44
+ Rails.logger.error "[ActiveCanvas] HTTP Basic Auth selected but credentials not configured"
45
+ render plain: "Authentication misconfigured", status: :forbidden
46
+ return
47
+ end
48
+
49
+ authenticate_or_request_with_http_basic("ActiveCanvas Admin") do |username, password|
50
+ ActiveSupport::SecurityUtils.secure_compare(username, config.http_basic_user) &
51
+ ActiveSupport::SecurityUtils.secure_compare(password, config.http_basic_password)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,61 @@
1
+ module ActiveCanvas
2
+ module Admin
3
+ class MediaController < ApplicationController
4
+ def index
5
+ @media = Media.images.recent
6
+
7
+ respond_to do |format|
8
+ format.html
9
+ format.json do
10
+ # Support pagination for the asset manager
11
+ page = (params[:page] || 1).to_i
12
+ per_page = (params[:per_page] || 20).to_i
13
+
14
+ total_count = @media.count
15
+ paginated_media = @media.offset((page - 1) * per_page).limit(per_page)
16
+
17
+ render json: {
18
+ data: paginated_media.map(&:as_json_for_editor),
19
+ meta: {
20
+ current_page: page,
21
+ per_page: per_page,
22
+ total_count: total_count,
23
+ total_pages: (total_count.to_f / per_page).ceil
24
+ }
25
+ }
26
+ end
27
+ end
28
+ end
29
+
30
+ def show
31
+ @medium = Media.find(params[:id])
32
+ end
33
+
34
+ def create
35
+ @media = Media.new(media_params)
36
+
37
+ if @media.save
38
+ render json: @media.as_json_for_editor, status: :created
39
+ else
40
+ render json: { errors: @media.errors.full_messages }, status: :unprocessable_entity
41
+ end
42
+ end
43
+
44
+ def destroy
45
+ @media = Media.find(params[:id])
46
+ @media.destroy
47
+
48
+ respond_to do |format|
49
+ format.html { redirect_to admin_media_url, notice: "Media deleted successfully." }
50
+ format.json { head :no_content }
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def media_params
57
+ params.require(:media).permit(:file, :filename)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ module ActiveCanvas
2
+ module Admin
3
+ class PageTypesController < ApplicationController
4
+ before_action :set_page_type, only: %i[show edit update destroy]
5
+
6
+ def index
7
+ @page_types = ActiveCanvas::PageType.order(:name)
8
+ end
9
+
10
+ def show
11
+ end
12
+
13
+ def new
14
+ @page_type = ActiveCanvas::PageType.new
15
+ end
16
+
17
+ def edit
18
+ end
19
+
20
+ def create
21
+ @page_type = ActiveCanvas::PageType.new(page_type_params)
22
+
23
+ if @page_type.save
24
+ redirect_to admin_page_type_path(@page_type), notice: "Page type was successfully created."
25
+ else
26
+ render :new, status: :unprocessable_entity
27
+ end
28
+ end
29
+
30
+ def update
31
+ if @page_type.update(page_type_params)
32
+ redirect_to admin_page_type_path(@page_type), notice: "Page type was successfully updated."
33
+ else
34
+ render :edit, status: :unprocessable_entity
35
+ end
36
+ end
37
+
38
+ def destroy
39
+ if @page_type.destroy
40
+ redirect_to admin_page_types_path, notice: "Page type was successfully deleted."
41
+ else
42
+ redirect_to admin_page_types_path, alert: @page_type.errors.full_messages.join(", ")
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def set_page_type
49
+ @page_type = ActiveCanvas::PageType.find(params[:id])
50
+ end
51
+
52
+ def page_type_params
53
+ params.require(:page_type).permit(:name, :key)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveCanvas
2
+ module Admin
3
+ class PageVersionsController < ApplicationController
4
+ before_action :set_page
5
+ before_action :set_version
6
+
7
+ def show
8
+ @previous_version = @page.versions.where("version_number < ?", @version.version_number).order(version_number: :desc).first
9
+ @next_version = @page.versions.where("version_number > ?", @version.version_number).order(version_number: :asc).first
10
+ end
11
+
12
+ private
13
+
14
+ def set_page
15
+ @page = Page.find(params[:page_id])
16
+ end
17
+
18
+ def set_version
19
+ @version = @page.versions.find(params[:id])
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,133 @@
1
+ module ActiveCanvas
2
+ module Admin
3
+ class PagesController < ApplicationController
4
+ include ActiveCanvas::TailwindCompilation
5
+
6
+ before_action :set_page, only: %i[show edit update destroy content update_content editor save_editor versions]
7
+
8
+ def index
9
+ @pages = ActiveCanvas::Page.includes(:page_type).order(created_at: :desc)
10
+ @media_count = ActiveCanvas::Media.count
11
+ @media_total_size = ActiveCanvas::Media.sum(:byte_size)
12
+ end
13
+
14
+ def show
15
+ end
16
+
17
+ def new
18
+ @page = ActiveCanvas::Page.new(page_type: ActiveCanvas::PageType.default)
19
+ end
20
+
21
+ def edit
22
+ end
23
+
24
+ def create
25
+ @page = ActiveCanvas::Page.new(page_params)
26
+
27
+ if @page.save
28
+ redirect_to admin_page_path(@page), notice: "Page was successfully created."
29
+ else
30
+ render :new, status: :unprocessable_entity
31
+ end
32
+ end
33
+
34
+ def update
35
+ if @page.update(page_params)
36
+ redirect_to admin_page_path(@page), notice: "Page was successfully updated."
37
+ else
38
+ render :edit, status: :unprocessable_entity
39
+ end
40
+ end
41
+
42
+ def destroy
43
+ @page.destroy
44
+ redirect_to admin_pages_path, notice: "Page was successfully deleted."
45
+ end
46
+
47
+ def content
48
+ end
49
+
50
+ def update_content
51
+ if @page.update(params.require(:page).permit(:content))
52
+ redirect_to content_admin_page_path(@page), notice: "Content saved."
53
+ else
54
+ render :content, status: :unprocessable_entity
55
+ end
56
+ end
57
+
58
+ def editor
59
+ respond_to do |format|
60
+ format.html { render layout: "active_canvas/admin/editor" }
61
+ format.json do
62
+ render json: {
63
+ content: @page.content,
64
+ content_css: @page.content_css,
65
+ content_js: @page.content_js,
66
+ content_components: @page.content_components
67
+ }
68
+ end
69
+ end
70
+ end
71
+
72
+ def save_editor
73
+ content_changed = @page.content != editor_params[:content]
74
+ Rails.logger.info "[ActiveCanvas::PagesController] save_editor for page ##{@page.id}"
75
+ Rails.logger.info "[ActiveCanvas::PagesController] content_changed: #{content_changed}"
76
+
77
+ if @page.update(editor_params)
78
+ tailwind_info = compile_tailwind_if_needed(content_changed) do
79
+ compiled_css = ActiveCanvas::TailwindCompiler.compile_for_page(@page)
80
+ @page.update_columns(compiled_tailwind_css: compiled_css, tailwind_compiled_at: Time.current)
81
+ compiled_css
82
+ end
83
+
84
+ respond_to do |format|
85
+ format.html { redirect_to editor_admin_page_path(@page), notice: "Page saved successfully." }
86
+ format.json do
87
+ render json: {
88
+ success: true,
89
+ message: "Page saved successfully.",
90
+ tailwind: tailwind_info
91
+ }
92
+ end
93
+ end
94
+ else
95
+ respond_to do |format|
96
+ format.html { render :editor, layout: "active_canvas/admin/editor", status: :unprocessable_entity }
97
+ format.json { render json: { success: false, errors: @page.errors.full_messages }, status: :unprocessable_entity }
98
+ end
99
+ end
100
+ end
101
+
102
+ def versions
103
+ @versions = @page.versions.recent.limit(50)
104
+ end
105
+
106
+ private
107
+
108
+ def set_page
109
+ @page = ActiveCanvas::Page.find(params[:id])
110
+ end
111
+
112
+ def page_params
113
+ params.require(:page).permit(
114
+ :title, :slug, :content, :page_type_id, :published,
115
+ # Header/Footer
116
+ :show_header, :show_footer,
117
+ # SEO fields
118
+ :meta_title, :meta_description, :canonical_url, :meta_robots,
119
+ # Open Graph fields
120
+ :og_title, :og_description, :og_image,
121
+ # Twitter fields
122
+ :twitter_card, :twitter_title, :twitter_description, :twitter_image,
123
+ # Structured data
124
+ :structured_data
125
+ )
126
+ end
127
+
128
+ def editor_params
129
+ params.require(:page).permit(:content, :content_css, :content_js, :content_components)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,88 @@
1
+ module ActiveCanvas
2
+ module Admin
3
+ class PartialsController < ApplicationController
4
+ include ActiveCanvas::TailwindCompilation
5
+
6
+ before_action :ensure_partials_exist, only: [:index]
7
+ before_action :set_partial, only: %i[edit update editor save_editor]
8
+
9
+ def index
10
+ @partials = ActiveCanvas::Partial.order(:partial_type)
11
+ end
12
+
13
+ def edit
14
+ end
15
+
16
+ def update
17
+ if @partial.update(partial_params)
18
+ redirect_to admin_partials_path, notice: "#{@partial.name} was successfully updated."
19
+ else
20
+ render :edit, status: :unprocessable_entity
21
+ end
22
+ end
23
+
24
+ def editor
25
+ respond_to do |format|
26
+ format.html { render layout: "active_canvas/admin/editor" }
27
+ format.json do
28
+ render json: {
29
+ content: @partial.content,
30
+ content_css: @partial.content_css,
31
+ content_js: @partial.content_js,
32
+ content_components: @partial.content_components
33
+ }
34
+ end
35
+ end
36
+ end
37
+
38
+ def save_editor
39
+ content_changed = @partial.content != editor_params[:content]
40
+
41
+ if @partial.update(editor_params)
42
+ tailwind_info = compile_tailwind_if_needed(content_changed) do
43
+ compiled_css = ActiveCanvas::TailwindCompiler.compile(
44
+ @partial.content.to_s,
45
+ identifier: "partial ##{@partial.id} (#{@partial.name})"
46
+ )
47
+ @partial.update_columns(compiled_css: compiled_css)
48
+ compiled_css
49
+ end
50
+
51
+ respond_to do |format|
52
+ format.html { redirect_to editor_admin_partial_path(@partial), notice: "#{@partial.name} saved successfully." }
53
+ format.json do
54
+ render json: {
55
+ success: true,
56
+ message: "#{@partial.name} saved successfully.",
57
+ tailwind: tailwind_info
58
+ }
59
+ end
60
+ end
61
+ else
62
+ respond_to do |format|
63
+ format.html { render :editor, layout: "active_canvas/admin/editor", status: :unprocessable_entity }
64
+ format.json { render json: { success: false, errors: @partial.errors.full_messages }, status: :unprocessable_entity }
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def ensure_partials_exist
72
+ ActiveCanvas::Partial.ensure_defaults!
73
+ end
74
+
75
+ def set_partial
76
+ @partial = ActiveCanvas::Partial.find(params[:id])
77
+ end
78
+
79
+ def partial_params
80
+ params.require(:partial).permit(:name, :active)
81
+ end
82
+
83
+ def editor_params
84
+ params.require(:partial).permit(:content, :content_css, :content_js, :content_components)
85
+ end
86
+ end
87
+ end
88
+ end