collavre 0.3.2 → 0.5.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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/actiontext.css +73 -71
- data/app/assets/stylesheets/collavre/activity_logs.css +18 -45
- data/app/assets/stylesheets/collavre/comments_popup.css +197 -35
- data/app/assets/stylesheets/collavre/creatives.css +101 -51
- data/app/assets/stylesheets/collavre/dark_mode.css +221 -88
- data/app/assets/stylesheets/collavre/design_tokens.css +334 -0
- data/app/assets/stylesheets/collavre/mention_menu.css +13 -9
- data/app/assets/stylesheets/collavre/popup.css +57 -27
- data/app/assets/stylesheets/collavre/slide_view.css +6 -6
- data/app/assets/stylesheets/collavre/user_menu.css +4 -5
- data/app/components/collavre/plans_timeline_component.html.erb +2 -2
- data/app/controllers/collavre/admin/orchestration_controller.rb +9 -2
- data/app/controllers/collavre/admin/settings_controller.rb +199 -0
- data/app/controllers/collavre/comments/reactions_controller.rb +1 -9
- data/app/controllers/collavre/comments_controller.rb +39 -162
- data/app/controllers/collavre/creatives_controller.rb +18 -58
- data/app/controllers/collavre/users_controller.rb +31 -3
- data/app/helpers/collavre/application_helper.rb +97 -0
- data/app/helpers/collavre/creatives_helper.rb +10 -202
- data/app/javascript/collavre.js +0 -1
- data/app/javascript/components/creative_tree_row.js +3 -2
- data/app/javascript/controllers/comment_controller.js +309 -4
- data/app/javascript/controllers/comments/form_controller.js +52 -0
- data/app/javascript/controllers/comments/presence_controller.js +13 -0
- data/app/javascript/controllers/creatives/tree_controller.js +2 -1
- data/app/javascript/controllers/link_creative_controller.js +29 -3
- data/app/javascript/lib/__tests__/html_code_block_wrapper.test.js +201 -0
- data/app/javascript/lib/html_code_block_wrapper.js +168 -0
- data/app/javascript/lib/utils/markdown.js +2 -1
- data/app/javascript/modules/creative_row_editor.js +5 -1
- data/app/javascript/utils/emoji_parser.js +21 -0
- data/app/jobs/collavre/ai_agent_job.rb +6 -2
- data/app/jobs/collavre/cron_action_job.rb +18 -6
- data/app/jobs/collavre/cron_scheduler_job.rb +112 -0
- data/app/models/collavre/comment/approvable.rb +50 -0
- data/app/models/collavre/comment/broadcastable.rb +119 -0
- data/app/models/collavre/comment/notifiable.rb +111 -0
- data/app/models/collavre/comment.rb +13 -258
- data/app/models/collavre/comment_reaction.rb +15 -0
- data/app/models/collavre/creative/describable.rb +86 -0
- data/app/models/collavre/creative/linkable.rb +77 -0
- data/app/models/collavre/creative/permissible.rb +103 -0
- data/app/models/collavre/creative.rb +3 -289
- data/app/models/collavre/orchestrator_policy.rb +1 -1
- data/app/models/collavre/system_setting.rb +27 -1
- data/app/models/collavre/user.rb +42 -0
- data/app/models/collavre/user_theme.rb +10 -0
- data/app/services/collavre/ai_agent/approval_handler.rb +110 -0
- data/app/services/collavre/ai_agent/message_builder.rb +129 -0
- data/app/services/collavre/ai_agent/review_handler.rb +70 -0
- data/app/services/collavre/ai_agent_service.rb +93 -150
- data/app/services/collavre/ai_client.rb +23 -4
- data/app/services/collavre/auto_theme_generator.rb +168 -50
- data/app/services/collavre/command_menu_service.rb +70 -0
- data/app/services/collavre/comment_move_service.rb +94 -0
- data/app/services/collavre/comments/action_executor.rb +10 -0
- data/app/services/collavre/comments/mcp_command.rb +1 -2
- data/app/services/collavre/creatives/create_service.rb +86 -0
- data/app/services/collavre/creatives/destroy_service.rb +41 -0
- data/app/services/collavre/creatives/index_query.rb +3 -0
- data/app/services/collavre/markdown_converter.rb +240 -0
- data/app/services/collavre/mention_parser.rb +63 -0
- data/app/services/collavre/orchestration/agent_context_builder.rb +24 -8
- data/app/services/collavre/orchestration/agent_orchestrator.rb +59 -10
- data/app/services/collavre/orchestration/loop_breaker.rb +12 -7
- data/app/services/collavre/orchestration/policy_resolver.rb +16 -2
- data/app/services/collavre/orchestration/scheduler.rb +4 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
- data/app/services/collavre/system_events/context_builder.rb +1 -6
- data/app/services/collavre/tools/creative_batch_service.rb +107 -0
- data/app/services/collavre/tools/creative_update_service.rb +17 -12
- data/app/services/collavre/tools/cron_create_service.rb +17 -5
- data/app/views/admin/shared/_tabs.html.erb +2 -1
- data/app/views/collavre/admin/orchestration/show.html.erb +11 -0
- data/app/views/collavre/admin/settings/_system_tab.html.erb +138 -0
- data/app/views/collavre/admin/settings/_uiux_tab.html.erb +44 -0
- data/app/views/collavre/admin/settings/index.html.erb +11 -0
- data/app/views/collavre/admin/settings/uiux.html.erb +11 -0
- data/app/views/collavre/comments/_comment.html.erb +15 -5
- data/app/views/collavre/comments/_comments_popup.html.erb +9 -2
- data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +0 -3
- data/app/views/collavre/creatives/_share_button.html.erb +0 -52
- data/app/views/collavre/creatives/_share_modal.html.erb +52 -0
- data/app/views/collavre/creatives/index.html.erb +5 -8
- data/app/views/collavre/shared/navigation/_panels.html.erb +2 -2
- data/app/views/collavre/user_themes/index.html.erb +7 -9
- data/app/views/collavre/users/_contact_management.html.erb +2 -1
- data/app/views/collavre/users/edit_ai.html.erb +7 -0
- data/app/views/collavre/users/index.html.erb +16 -1
- data/app/views/collavre/users/new_ai.html.erb +18 -8
- data/app/views/collavre/users/passkeys.html.erb +1 -1
- data/app/views/collavre/users/show.html.erb +1 -1
- data/app/views/layouts/collavre/slide.html.erb +8 -1
- data/config/locales/admin.en.yml +88 -0
- data/config/locales/admin.ko.yml +88 -0
- data/config/locales/ai_agent.en.yml +5 -1
- data/config/locales/ai_agent.ko.yml +5 -1
- data/config/locales/comments.en.yml +5 -1
- data/config/locales/comments.ko.yml +5 -1
- data/config/locales/orchestration.en.yml +8 -0
- data/config/locales/orchestration.ko.yml +8 -0
- data/config/locales/users.en.yml +12 -0
- data/config/locales/users.ko.yml +12 -0
- data/config/routes.rb +7 -1
- data/db/migrate/20260212011655_add_quoted_comment_to_comments.rb +7 -0
- data/db/migrate/20260213044247_add_agent_conf_to_users.rb +5 -0
- data/lib/collavre/engine.rb +25 -0
- data/lib/collavre/version.rb +1 -1
- metadata +32 -1
|
@@ -163,23 +163,34 @@ module Collavre
|
|
|
163
163
|
if @comment.save
|
|
164
164
|
|
|
165
165
|
# Dispatch system event
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
166
|
+
unless @comment.private? || response.present?
|
|
167
|
+
begin
|
|
168
|
+
::SystemEvents::Dispatcher.dispatch("comment_created", {
|
|
169
|
+
comment: {
|
|
170
|
+
id: @comment.id,
|
|
171
|
+
content: @comment.content,
|
|
172
|
+
user_id: @comment.user_id,
|
|
173
|
+
from_ai: @comment.user&.searchable? || false,
|
|
174
|
+
quoted_comment_id: @comment.quoted_comment_id
|
|
175
|
+
}.compact,
|
|
176
|
+
creative: {
|
|
177
|
+
id: @creative.id,
|
|
178
|
+
description: @creative.description
|
|
179
|
+
},
|
|
180
|
+
topic: {
|
|
181
|
+
id: @comment.topic_id
|
|
182
|
+
},
|
|
183
|
+
chat: {
|
|
184
|
+
content: @comment.content
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
rescue => e
|
|
188
|
+
Rails.logger.error(
|
|
189
|
+
"[SystemEvents] Dispatch failed for comment #{@comment.id}: " \
|
|
190
|
+
"#{e.class} #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}"
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
183
194
|
@comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
|
|
184
195
|
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }, status: :created
|
|
185
196
|
else
|
|
@@ -189,7 +200,7 @@ module Collavre
|
|
|
189
200
|
|
|
190
201
|
def update
|
|
191
202
|
if @comment.user == Current.user
|
|
192
|
-
safe_params = comment_params
|
|
203
|
+
safe_params = comment_params.except(:quoted_comment_id, :quoted_text)
|
|
193
204
|
if safe_params[:topic_id].present? && !@creative.topics.where(id: safe_params[:topic_id]).exists?
|
|
194
205
|
render json: { error: I18n.t("collavre.comments.invalid_topic") }, status: :unprocessable_entity and return
|
|
195
206
|
end
|
|
@@ -392,87 +403,19 @@ module Collavre
|
|
|
392
403
|
head :forbidden and return
|
|
393
404
|
end
|
|
394
405
|
|
|
395
|
-
render json:
|
|
406
|
+
render json: CommandMenuService.new(user: Current.user).items
|
|
396
407
|
end
|
|
397
408
|
|
|
398
409
|
def move
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
# Support moving to topic within same creative (target_topic_id only)
|
|
405
|
-
# or moving to different creative (target_creative_id)
|
|
406
|
-
target_topic_id = params[:target_topic_id]
|
|
407
|
-
target_creative_id = params[:target_creative_id]
|
|
408
|
-
|
|
409
|
-
# Determine target creative and topic
|
|
410
|
-
if target_creative_id.present?
|
|
411
|
-
# Moving to different creative
|
|
412
|
-
target_creative = Creative.find_by(id: target_creative_id)
|
|
413
|
-
if target_creative.nil?
|
|
414
|
-
render json: { error: I18n.t("collavre.comments.move_invalid_target") }, status: :unprocessable_entity and return
|
|
415
|
-
end
|
|
416
|
-
target_origin = target_creative.effective_origin
|
|
417
|
-
new_topic_id = nil # Reset topic when moving to different creative
|
|
418
|
-
elsif target_topic_id.present? || target_topic_id == ""
|
|
419
|
-
# Moving to topic within same creative (empty string means Main/no topic)
|
|
420
|
-
target_origin = @creative
|
|
421
|
-
new_topic_id = target_topic_id.presence # nil for Main topic
|
|
422
|
-
|
|
423
|
-
# Validate topic exists if specified
|
|
424
|
-
if new_topic_id.present? && !@creative.topics.exists?(id: new_topic_id)
|
|
425
|
-
render json: { error: I18n.t("collavre.comments.move_invalid_topic", default: "Invalid topic") }, status: :unprocessable_entity and return
|
|
426
|
-
end
|
|
427
|
-
else
|
|
428
|
-
render json: { error: I18n.t("collavre.comments.move_invalid_target") }, status: :unprocessable_entity and return
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
unless @creative.has_permission?(Current.user, :feedback) && target_origin.has_permission?(Current.user, :feedback)
|
|
432
|
-
render json: { error: I18n.t("collavre.comments.move_not_allowed") }, status: :forbidden and return
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
scope = @creative.comments.where(
|
|
436
|
-
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
437
|
-
false,
|
|
438
|
-
Current.user.id,
|
|
439
|
-
Current.user.id
|
|
410
|
+
result = CommentMoveService.new(creative: @creative, user: Current.user).call(
|
|
411
|
+
comment_ids: params[:comment_ids],
|
|
412
|
+
target_creative_id: params[:target_creative_id],
|
|
413
|
+
target_topic_id: params[:target_topic_id]
|
|
440
414
|
)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
render json: { error: I18n.t("collavre.comments.move_not_allowed") }, status: :forbidden and return
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
moved_count = 0
|
|
449
|
-
ActiveRecord::Base.transaction do
|
|
450
|
-
comments.each do |comment|
|
|
451
|
-
# Skip if already in target location
|
|
452
|
-
same_creative = comment.creative_id == target_origin.id
|
|
453
|
-
same_topic = comment.topic_id.to_s == new_topic_id.to_s
|
|
454
|
-
|
|
455
|
-
next if same_creative && same_topic
|
|
456
|
-
|
|
457
|
-
original_creative = comment.creative
|
|
458
|
-
original_topic_id = comment.topic_id
|
|
459
|
-
|
|
460
|
-
if same_creative
|
|
461
|
-
# Just update topic within same creative
|
|
462
|
-
comment.update!(topic_id: new_topic_id)
|
|
463
|
-
else
|
|
464
|
-
# Move to different creative
|
|
465
|
-
comment.update!(creative: target_origin, topic_id: new_topic_id)
|
|
466
|
-
broadcast_move_removal(comment, original_creative)
|
|
467
|
-
end
|
|
468
|
-
moved_count += 1
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
Comment.broadcast_badges(@creative)
|
|
473
|
-
Comment.broadcast_badges(target_origin) unless target_origin == @creative
|
|
474
|
-
|
|
475
|
-
render json: { success: true, moved_count: moved_count }
|
|
415
|
+
render json: result
|
|
416
|
+
rescue CommentMoveService::MoveError => e
|
|
417
|
+
status = e.message == I18n.t("collavre.comments.move_not_allowed") ? :forbidden : :unprocessable_entity
|
|
418
|
+
render json: { error: e.message }, status: status
|
|
476
419
|
rescue ActiveRecord::RecordInvalid => e
|
|
477
420
|
render json: { error: e.record.errors.full_messages.to_sentence.presence || I18n.t("collavre.comments.move_error") }, status: :unprocessable_entity
|
|
478
421
|
end
|
|
@@ -483,63 +426,6 @@ module Collavre
|
|
|
483
426
|
@creative = Creative.find(params[:creative_id]).effective_origin
|
|
484
427
|
end
|
|
485
428
|
|
|
486
|
-
def command_menu_items
|
|
487
|
-
[
|
|
488
|
-
{
|
|
489
|
-
name: "calendar",
|
|
490
|
-
label: "/calendar",
|
|
491
|
-
aliases: [ "/cal" ],
|
|
492
|
-
description: I18n.t("collavre.comments.command_menu.calendar_description"),
|
|
493
|
-
args: I18n.t("collavre.comments.command_menu.calendar_args")
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
name: "topic",
|
|
497
|
-
label: "/topic",
|
|
498
|
-
description: I18n.t("collavre.comments.command_menu.topic_description"),
|
|
499
|
-
args: I18n.t("collavre.comments.command_menu.topic_args")
|
|
500
|
-
}
|
|
501
|
-
] + mcp_command_items
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
def mcp_command_items
|
|
505
|
-
Collavre::McpService.available_tools(Current.user).filter_map do |tool|
|
|
506
|
-
tool_name = tool[:name] || tool["name"]
|
|
507
|
-
next unless tool_name
|
|
508
|
-
|
|
509
|
-
{
|
|
510
|
-
name: tool_name,
|
|
511
|
-
label: "/#{tool_name}",
|
|
512
|
-
description: tool[:description] || tool["description"],
|
|
513
|
-
args: format_command_args(tool[:params] || tool["params"])
|
|
514
|
-
}
|
|
515
|
-
end
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
def format_command_args(params)
|
|
519
|
-
return if params.blank?
|
|
520
|
-
|
|
521
|
-
# Handle array format (from MetaToolService)
|
|
522
|
-
if params.is_a?(Array)
|
|
523
|
-
return params.map do |param|
|
|
524
|
-
name = param[:name] || param["name"]
|
|
525
|
-
required = param[:required] || param["required"]
|
|
526
|
-
name.to_s + (required ? "*" : "")
|
|
527
|
-
end.join(", ")
|
|
528
|
-
end
|
|
529
|
-
|
|
530
|
-
# Handle JSON Schema format (Hash with :properties)
|
|
531
|
-
properties = params[:properties] || params["properties"]
|
|
532
|
-
return unless properties.is_a?(Hash)
|
|
533
|
-
|
|
534
|
-
required = params[:required] || params["required"] || []
|
|
535
|
-
required = Array(required).map(&:to_s)
|
|
536
|
-
|
|
537
|
-
properties.keys.map do |key|
|
|
538
|
-
key = key.to_s
|
|
539
|
-
required.include?(key) ? "#{key}*" : key
|
|
540
|
-
end.join(", ")
|
|
541
|
-
end
|
|
542
|
-
|
|
543
429
|
def set_comment
|
|
544
430
|
@comment = @creative.comments
|
|
545
431
|
.where(
|
|
@@ -552,22 +438,13 @@ module Collavre
|
|
|
552
438
|
end
|
|
553
439
|
|
|
554
440
|
def comment_params
|
|
555
|
-
params.require(:comment).permit(:content, :private, :topic_id, images: [])
|
|
441
|
+
params.require(:comment).permit(:content, :private, :topic_id, :quoted_comment_id, :quoted_text, images: [])
|
|
556
442
|
end
|
|
557
443
|
|
|
558
444
|
def can_convert_comment?
|
|
559
445
|
@comment.user == Current.user || @creative.has_permission?(Current.user, :admin)
|
|
560
446
|
end
|
|
561
447
|
|
|
562
|
-
def broadcast_move_removal(comment, original_creative)
|
|
563
|
-
return if comment.private?
|
|
564
|
-
|
|
565
|
-
Turbo::StreamsChannel.broadcast_remove_to(
|
|
566
|
-
[ original_creative, :comments ],
|
|
567
|
-
target: view_context.dom_id(comment)
|
|
568
|
-
)
|
|
569
|
-
end
|
|
570
|
-
|
|
571
448
|
def build_convert_system_message(creative)
|
|
572
449
|
title = helpers.strip_tags(creative.description).to_s.strip
|
|
573
450
|
title = I18n.t("collavre.comments.convert_system_message_default_title") if title.blank?
|
|
@@ -176,47 +176,21 @@ module Collavre
|
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
def create
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if @creative.save
|
|
192
|
-
@child_creative.update(parent: @creative) if @child_creative
|
|
193
|
-
if params[:before_id].present?
|
|
194
|
-
before_creative = Creative.find_by(id: params[:before_id])
|
|
195
|
-
if before_creative && before_creative.parent_id == @creative.parent_id
|
|
196
|
-
siblings = @creative.parent ? @creative.parent.children.order(:sequence).to_a : Creative.roots.order(:sequence).to_a
|
|
197
|
-
siblings.reject! { |s| s.id == @creative.id }
|
|
198
|
-
index = siblings.index { |s| s.id == before_creative.id } || 0
|
|
199
|
-
siblings.insert(index, @creative)
|
|
200
|
-
siblings.each_with_index { |c, idx| c.update_column(:sequence, idx) }
|
|
201
|
-
end
|
|
202
|
-
elsif params[:after_id].present?
|
|
203
|
-
after_creative = Creative.find_by(id: params[:after_id])
|
|
204
|
-
if after_creative && after_creative.parent_id == @creative.parent_id
|
|
205
|
-
siblings = @creative.parent ? @creative.parent.children.order(:sequence).to_a : Creative.roots.order(:sequence).to_a
|
|
206
|
-
siblings.reject! { |s| s.id == @creative.id }
|
|
207
|
-
index = siblings.index { |s| s.id == after_creative.id } || -1
|
|
208
|
-
siblings.insert(index + 1, @creative)
|
|
209
|
-
siblings.each_with_index { |c, idx| c.update_column(:sequence, idx) }
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
if params[:tags].present?
|
|
213
|
-
Array(params[:tags]).each do |tag_id|
|
|
214
|
-
@creative.tags.create(label_id: tag_id)
|
|
215
|
-
end
|
|
216
|
-
end
|
|
179
|
+
result = Creatives::CreateService.new(
|
|
180
|
+
creative_params: creative_params,
|
|
181
|
+
user: Current.user,
|
|
182
|
+
child_id: params[:child_id],
|
|
183
|
+
before_id: params[:before_id],
|
|
184
|
+
after_id: params[:after_id],
|
|
185
|
+
tag_ids: params[:tags]
|
|
186
|
+
).call
|
|
187
|
+
|
|
188
|
+
@creative = result.creative
|
|
189
|
+
|
|
190
|
+
if result.success?
|
|
217
191
|
render json: { id: @creative.id }
|
|
218
192
|
else
|
|
219
|
-
render json: { errors:
|
|
193
|
+
render json: { errors: result.errors }, status: :unprocessable_entity
|
|
220
194
|
end
|
|
221
195
|
end
|
|
222
196
|
|
|
@@ -275,15 +249,11 @@ module Collavre
|
|
|
275
249
|
unless @creative.has_permission?(Current.user, :admin)
|
|
276
250
|
redirect_to @creative, alert: t("collavre.creatives.errors.no_permission") and return
|
|
277
251
|
end
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
@creative.children.each { |child| child.update(parent: parent) }
|
|
284
|
-
end
|
|
285
|
-
CreativeShare.where(creative: @creative).destroy_all
|
|
286
|
-
@creative.destroy
|
|
252
|
+
Creatives::DestroyService.new(
|
|
253
|
+
creative: @creative,
|
|
254
|
+
user: Current.user,
|
|
255
|
+
delete_with_children: params[:delete_with_children].present?
|
|
256
|
+
).call
|
|
287
257
|
end
|
|
288
258
|
|
|
289
259
|
def request_permission
|
|
@@ -524,16 +494,6 @@ module Collavre
|
|
|
524
494
|
}
|
|
525
495
|
end
|
|
526
496
|
|
|
527
|
-
# Recursively destroy all descendants the user can delete
|
|
528
|
-
def destroy_descendants_recursively(creative, user)
|
|
529
|
-
deletable_children = creative.children_with_permission(user, :admin)
|
|
530
|
-
deletable_children.each do |child|
|
|
531
|
-
destroy_descendants_recursively(child, user)
|
|
532
|
-
CreativeShare.where(creative: child).destroy_all
|
|
533
|
-
child.destroy
|
|
534
|
-
end
|
|
535
|
-
end
|
|
536
|
-
|
|
537
497
|
def enforce_creatives_login_policy
|
|
538
498
|
if SystemSetting.creatives_login_required?
|
|
539
499
|
require_authentication
|
|
@@ -2,7 +2,7 @@ module Collavre
|
|
|
2
2
|
class UsersController < ApplicationController
|
|
3
3
|
allow_unauthenticated_access only: %i[new create exists]
|
|
4
4
|
before_action -> { enforce_auth_provider!(:email) }, only: [ :new, :create ]
|
|
5
|
-
before_action :require_system_admin!, only: [ :index, :grant_system_admin, :revoke_system_admin ]
|
|
5
|
+
before_action :require_system_admin!, only: [ :index, :grant_system_admin, :revoke_system_admin, :unlock, :lock ]
|
|
6
6
|
|
|
7
7
|
def new
|
|
8
8
|
@user = Collavre::User.new
|
|
@@ -53,6 +53,14 @@ module Collavre
|
|
|
53
53
|
|
|
54
54
|
def new_ai
|
|
55
55
|
@available_tools = load_available_tools
|
|
56
|
+
|
|
57
|
+
if params[:copy_from].present?
|
|
58
|
+
source = Collavre::User.find_by(id: params[:copy_from])
|
|
59
|
+
if source&.ai_user? && source.created_by_id == Current.user.id
|
|
60
|
+
@copy_source = source
|
|
61
|
+
@copy_name = "#{source.name} (copy)"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
def create_ai
|
|
@@ -75,6 +83,7 @@ module Collavre
|
|
|
75
83
|
created_by_id: Current.user.id,
|
|
76
84
|
routing_expression: params[:routing_expression]
|
|
77
85
|
)
|
|
86
|
+
@user.agent_conf = params[:agent_conf] if @user.respond_to?(:agent_conf=) && params[:agent_conf].present?
|
|
78
87
|
|
|
79
88
|
if @user.save
|
|
80
89
|
Collavre::Contact.ensure(user: Current.user, contact_user: @user)
|
|
@@ -112,7 +121,7 @@ module Collavre
|
|
|
112
121
|
return
|
|
113
122
|
end
|
|
114
123
|
|
|
115
|
-
ai_params = params.require(:user).permit(:name, :system_prompt, :llm_vendor, :llm_model, :llm_api_key, :gateway_url, :searchable, :routing_expression, tools: [])
|
|
124
|
+
ai_params = params.require(:user).permit(:name, :system_prompt, :llm_vendor, :llm_model, :llm_api_key, :gateway_url, :searchable, :routing_expression, :agent_conf, tools: [])
|
|
116
125
|
|
|
117
126
|
if @user.update(ai_params)
|
|
118
127
|
redirect_to edit_ai_user_path(@user), notice: I18n.t("collavre.users.update_ai.success")
|
|
@@ -137,7 +146,10 @@ module Collavre
|
|
|
137
146
|
end
|
|
138
147
|
|
|
139
148
|
scope = if params[:scope] == "contacts" && Current.user
|
|
140
|
-
|
|
149
|
+
# Include both contacts and searchable users (e.g., AI agents with searchable=true)
|
|
150
|
+
contact_ids = Current.user.contact_users.select(:id)
|
|
151
|
+
searchable_ids = Collavre::User.where(searchable: true).select(:id)
|
|
152
|
+
Collavre::User.where(id: contact_ids).or(Collavre::User.where(id: searchable_ids))
|
|
141
153
|
else
|
|
142
154
|
Collavre::User.mentionable_for(creative)
|
|
143
155
|
end
|
|
@@ -220,6 +232,22 @@ module Collavre
|
|
|
220
232
|
end
|
|
221
233
|
end
|
|
222
234
|
|
|
235
|
+
def unlock
|
|
236
|
+
@user = Collavre::User.find(params[:id])
|
|
237
|
+
@user.unlock_account!
|
|
238
|
+
redirect_to users_path, notice: I18n.t("collavre.users.unlock.success", name: @user.display_name)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def lock
|
|
242
|
+
@user = Collavre::User.find(params[:id])
|
|
243
|
+
if @user == Current.user
|
|
244
|
+
redirect_to users_path, alert: I18n.t("collavre.users.lock.cannot_lock_self")
|
|
245
|
+
return
|
|
246
|
+
end
|
|
247
|
+
@user.lock_account!
|
|
248
|
+
redirect_to users_path, notice: I18n.t("collavre.users.lock.success", name: @user.display_name)
|
|
249
|
+
end
|
|
250
|
+
|
|
223
251
|
def update
|
|
224
252
|
@user = Collavre::User.find(params[:id])
|
|
225
253
|
if @user.update(profile_params)
|
|
@@ -1,4 +1,101 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
# Returns the body CSS class for the current user's theme.
|
|
4
|
+
# Custom themes get "light-mode" to disable OS dark mode overrides,
|
|
5
|
+
# ensuring the custom theme controls all colors regardless of OS setting.
|
|
6
|
+
def body_theme_class
|
|
7
|
+
theme = Current.user&.theme
|
|
8
|
+
return "" if theme.blank?
|
|
9
|
+
|
|
10
|
+
case theme
|
|
11
|
+
when "dark"
|
|
12
|
+
"dark-mode"
|
|
13
|
+
when "light"
|
|
14
|
+
"light-mode"
|
|
15
|
+
else
|
|
16
|
+
"light-mode" # Custom theme: neutralize OS dark mode
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Renders CSS for admin-configured default themes (light/dark mode)
|
|
21
|
+
# Only applies when the user has no personal theme set
|
|
22
|
+
def default_theme_styles
|
|
23
|
+
return "" if Current.user&.theme.present?
|
|
24
|
+
|
|
25
|
+
light_theme = SystemSetting.default_light_theme
|
|
26
|
+
dark_theme = SystemSetting.default_dark_theme
|
|
27
|
+
return "" unless light_theme || dark_theme
|
|
28
|
+
|
|
29
|
+
styles = []
|
|
30
|
+
|
|
31
|
+
if light_theme
|
|
32
|
+
styles << render_theme_media_query(light_theme, "light")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if dark_theme
|
|
36
|
+
styles << render_theme_media_query(dark_theme, "dark")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
styles.join("\n").html_safe # rubocop:disable Rails/OutputSafety
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def render_theme_media_query(theme, mode)
|
|
45
|
+
vars = theme.variables.map { |k, v| "#{k}: #{v} !important;" }.join("\n ")
|
|
46
|
+
legacy = legacy_alias_declarations(theme.variables).map { |k, v| "#{k}: #{v} !important;" }.join("\n ")
|
|
47
|
+
dark_filter = theme.dark? ? "--date-icon-filter: invert(0.8) !important;" : ""
|
|
48
|
+
|
|
49
|
+
<<~CSS
|
|
50
|
+
@media (prefers-color-scheme: #{mode}) {
|
|
51
|
+
body:not(.dark-mode):not(.light-mode) {
|
|
52
|
+
#{vars}
|
|
53
|
+
#{legacy}
|
|
54
|
+
#{dark_filter}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
CSS
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
public
|
|
61
|
+
|
|
62
|
+
# Maps semantic token names to their legacy alias names.
|
|
63
|
+
# When custom themes inject semantic tokens on <body>, the legacy aliases
|
|
64
|
+
# defined on :root still resolve to :root's light values. This map lets
|
|
65
|
+
# the theme injection template also emit legacy aliases so that CSS using
|
|
66
|
+
# old variable names (e.g. --color-section-bg) picks up custom values.
|
|
67
|
+
SEMANTIC_TO_LEGACY = {
|
|
68
|
+
"--surface-bg" => "--color-bg",
|
|
69
|
+
"--surface-nav" => "--color-nav-bg",
|
|
70
|
+
"--surface-section" => "--color-section-bg",
|
|
71
|
+
"--surface-input" => "--color-input-bg",
|
|
72
|
+
"--surface-btn" => "--color-btn-bg",
|
|
73
|
+
"--surface-secondary" => "--color-secondary-background",
|
|
74
|
+
"--text-primary" => "--color-text",
|
|
75
|
+
"--text-muted" => "--color-muted",
|
|
76
|
+
"--text-on-btn" => "--color-btn-text",
|
|
77
|
+
"--text-nav" => "--color-nav-text",
|
|
78
|
+
"--text-nav-btn" => "--color-nav-btn-text",
|
|
79
|
+
"--text-chat-btn" => "--color-chat-btn-text",
|
|
80
|
+
"--text-on-badge" => "--color-badge-text",
|
|
81
|
+
"--text-input" => "--color-input-text",
|
|
82
|
+
"--color-brand" => "--color-complete",
|
|
83
|
+
"--color-active" => "--color-secondary-active",
|
|
84
|
+
"--border-color" => "--color-border",
|
|
85
|
+
"--border-drag-over" => "--color-drag-over",
|
|
86
|
+
"--border-drag-edge" => "--color-drag-over-edge"
|
|
87
|
+
}.freeze
|
|
88
|
+
|
|
89
|
+
# Given a hash of semantic token variables, returns additional legacy
|
|
90
|
+
# alias declarations so old CSS picks up the custom theme values.
|
|
91
|
+
def legacy_alias_declarations(variables)
|
|
92
|
+
aliases = {}
|
|
93
|
+
variables.each do |key, value|
|
|
94
|
+
if (legacy_name = SEMANTIC_TO_LEGACY[key])
|
|
95
|
+
aliases[legacy_name] = value
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
aliases
|
|
99
|
+
end
|
|
3
100
|
end
|
|
4
101
|
end
|