collavre 0.1.1 → 0.2.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/comments_popup.css +293 -8
- data/app/assets/stylesheets/collavre/mention_menu.css +26 -0
- data/app/assets/stylesheets/collavre/popup.css +7 -0
- data/app/assets/stylesheets/collavre/print.css +18 -0
- data/app/channels/collavre/comments_presence_channel.rb +33 -0
- data/app/components/collavre/autocomplete_popup_component.html.erb +3 -0
- data/app/components/collavre/autocomplete_popup_component.rb +18 -0
- data/app/components/collavre/command_menu_component.rb +7 -0
- data/app/components/collavre/plans_timeline_component.html.erb +1 -1
- data/app/components/collavre/plans_timeline_component.rb +29 -32
- data/app/components/collavre/user_mention_menu_component.rb +4 -5
- data/app/controllers/collavre/comments_controller.rb +111 -10
- data/app/controllers/collavre/creatives_controller.rb +8 -0
- data/app/controllers/collavre/google_auth_controller.rb +5 -1
- data/app/controllers/collavre/plans_controller.rb +65 -9
- data/app/controllers/collavre/topics_controller.rb +42 -0
- data/app/controllers/collavre/users_controller.rb +4 -14
- data/app/errors/collavre/approval_pending_error.rb +54 -0
- data/app/errors/collavre/cancelled_error.rb +9 -0
- data/app/helpers/collavre/navigation_helper.rb +3 -1
- data/app/javascript/collavre.js +1 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +2 -1
- data/app/javascript/controllers/comments/form_controller.js +2 -1
- data/app/javascript/controllers/comments/list_controller.js +185 -2
- data/app/javascript/controllers/comments/popup_controller.js +95 -20
- data/app/javascript/controllers/comments/presence_controller.js +30 -1
- data/app/javascript/controllers/comments/topics_controller.js +314 -4
- data/app/javascript/modules/__tests__/creative_progress.test.js +50 -0
- data/app/javascript/modules/command_menu.js +116 -0
- data/app/javascript/modules/creative_progress.js +14 -0
- data/app/javascript/modules/creative_row_editor.js +104 -20
- data/app/javascript/modules/plans_timeline.js +15 -4
- data/app/javascript/modules/share_modal.js +3 -0
- data/app/jobs/collavre/ai_agent_job.rb +35 -21
- data/app/models/collavre/calendar_event.rb +7 -1
- data/app/models/collavre/comment.rb +35 -2
- data/app/models/collavre/creative.rb +1 -3
- data/app/models/collavre/mcp_tool.rb +4 -0
- data/app/models/collavre/plan.rb +23 -0
- data/app/models/collavre/topic.rb +12 -0
- data/app/models/collavre/user.rb +15 -1
- data/app/services/collavre/ai_agent_service.rb +174 -66
- data/app/services/collavre/ai_client.rb +31 -2
- data/app/services/collavre/comments/action_executor.rb +47 -1
- data/app/services/collavre/comments/calendar_command.rb +117 -18
- data/app/services/collavre/google_calendar_service.rb +38 -15
- data/app/services/collavre/markdown_importer.rb +47 -8
- data/app/services/collavre/mcp_service.rb +23 -10
- data/app/services/collavre/system_events/router.rb +50 -26
- data/app/services/collavre/tools/creative_create_service.rb +97 -0
- data/app/services/collavre/tools/creative_update_service.rb +116 -0
- data/app/views/collavre/comments/_comment.html.erb +2 -2
- data/app/views/collavre/comments/_comments_popup.html.erb +40 -6
- data/app/views/collavre/comments/fullscreen.html.erb +5 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +11 -3
- data/app/views/collavre/creatives/_integration_modals.html.erb +6 -0
- data/app/views/collavre/creatives/_integration_triggers.html.erb +8 -0
- data/app/views/collavre/creatives/_integrations_menu.html.erb +12 -0
- data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +13 -1
- data/app/views/collavre/creatives/_share_button.html.erb +1 -1
- data/app/views/collavre/creatives/index.html.erb +22 -4
- data/app/views/collavre/users/edit_ai.html.erb +15 -0
- data/app/views/collavre/users/new_ai.html.erb +15 -0
- data/app/views/layouts/collavre/chat.html.erb +46 -0
- data/config/locales/ai_agent.en.yml +15 -0
- data/config/locales/ai_agent.ko.yml +15 -0
- data/config/locales/comments.en.yml +15 -3
- data/config/locales/comments.ko.yml +15 -3
- data/config/locales/creatives.en.yml +3 -31
- data/config/locales/creatives.ko.yml +3 -27
- data/config/locales/plans.en.yml +4 -0
- data/config/locales/plans.ko.yml +4 -0
- data/config/locales/users.en.yml +3 -0
- data/config/locales/users.ko.yml +3 -0
- data/config/routes.rb +8 -3
- data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +1 -1
- data/db/migrate/20260131100000_migrate_active_storage_attachment_record_types.rb +21 -0
- data/db/migrate/20260201100000_make_google_event_id_nullable.rb +5 -0
- data/lib/collavre/engine.rb +171 -6
- data/lib/collavre/integration_registry.rb +129 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +2 -0
- data/lib/navigation/registry.rb +130 -0
- metadata +22 -15
- data/app/components/collavre/user_mention_menu_component.html.erb +0 -3
- data/app/controllers/collavre/notion_auth_controller.rb +0 -25
- data/app/jobs/collavre/notion_export_job.rb +0 -30
- data/app/jobs/collavre/notion_sync_job.rb +0 -48
- data/app/models/collavre/notion_account.rb +0 -17
- data/app/models/collavre/notion_block_link.rb +0 -10
- data/app/models/collavre/notion_page_link.rb +0 -19
- data/app/services/collavre/notion_client.rb +0 -231
- data/app/services/collavre/notion_creative_exporter.rb +0 -296
- data/app/services/collavre/notion_service.rb +0 -249
- data/app/views/collavre/creatives/_notion_integration_modal.html.erb +0 -90
- data/db/migrate/20241201000000_create_notion_integrations.rb +0 -29
- data/db/migrate/20250312000000_create_notion_block_links.rb +0 -16
- data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +0 -5
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CommentsController < ApplicationController
|
|
3
|
+
layout "collavre/chat", only: [ :fullscreen ]
|
|
3
4
|
before_action :set_creative
|
|
4
5
|
before_action :set_comment, only: [ :destroy, :show, :update, :convert, :approve, :update_action ]
|
|
5
6
|
|
|
7
|
+
def fullscreen
|
|
8
|
+
@creative_snippet = @creative.creative_snippet
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
def index
|
|
7
12
|
limit = 20
|
|
8
13
|
|
|
@@ -355,7 +360,7 @@ module Collavre
|
|
|
355
360
|
def participants
|
|
356
361
|
users = [ @creative.user ].compact + @creative.all_shared_users(:feedback).map(&:user)
|
|
357
362
|
users = users.uniq
|
|
358
|
-
|
|
363
|
+
user_data = users.map do |u|
|
|
359
364
|
{
|
|
360
365
|
id: u.id,
|
|
361
366
|
email: u.email,
|
|
@@ -365,7 +370,18 @@ module Collavre
|
|
|
365
370
|
initial: u.display_name[0].upcase
|
|
366
371
|
}
|
|
367
372
|
end
|
|
368
|
-
render json:
|
|
373
|
+
render json: {
|
|
374
|
+
users: user_data,
|
|
375
|
+
can_share: @creative.has_permission?(Current.user, :admin)
|
|
376
|
+
}
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def commands
|
|
380
|
+
unless @creative.has_permission?(Current.user, :read)
|
|
381
|
+
head :forbidden and return
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
render json: command_menu_items
|
|
369
385
|
end
|
|
370
386
|
|
|
371
387
|
def move
|
|
@@ -374,13 +390,33 @@ module Collavre
|
|
|
374
390
|
render json: { error: I18n.t("collavre.comments.move_no_selection") }, status: :unprocessable_entity and return
|
|
375
391
|
end
|
|
376
392
|
|
|
377
|
-
|
|
378
|
-
|
|
393
|
+
# Support moving to topic within same creative (target_topic_id only)
|
|
394
|
+
# or moving to different creative (target_creative_id)
|
|
395
|
+
target_topic_id = params[:target_topic_id]
|
|
396
|
+
target_creative_id = params[:target_creative_id]
|
|
397
|
+
|
|
398
|
+
# Determine target creative and topic
|
|
399
|
+
if target_creative_id.present?
|
|
400
|
+
# Moving to different creative
|
|
401
|
+
target_creative = Creative.find_by(id: target_creative_id)
|
|
402
|
+
if target_creative.nil?
|
|
403
|
+
render json: { error: I18n.t("collavre.comments.move_invalid_target") }, status: :unprocessable_entity and return
|
|
404
|
+
end
|
|
405
|
+
target_origin = target_creative.effective_origin
|
|
406
|
+
new_topic_id = nil # Reset topic when moving to different creative
|
|
407
|
+
elsif target_topic_id.present? || target_topic_id == ""
|
|
408
|
+
# Moving to topic within same creative (empty string means Main/no topic)
|
|
409
|
+
target_origin = @creative
|
|
410
|
+
new_topic_id = target_topic_id.presence # nil for Main topic
|
|
411
|
+
|
|
412
|
+
# Validate topic exists if specified
|
|
413
|
+
if new_topic_id.present? && !@creative.topics.exists?(id: new_topic_id)
|
|
414
|
+
render json: { error: I18n.t("collavre.comments.move_invalid_topic", default: "Invalid topic") }, status: :unprocessable_entity and return
|
|
415
|
+
end
|
|
416
|
+
else
|
|
379
417
|
render json: { error: I18n.t("collavre.comments.move_invalid_target") }, status: :unprocessable_entity and return
|
|
380
418
|
end
|
|
381
419
|
|
|
382
|
-
target_origin = target_creative.effective_origin
|
|
383
|
-
|
|
384
420
|
unless @creative.has_permission?(Current.user, :feedback) && target_origin.has_permission?(Current.user, :feedback)
|
|
385
421
|
render json: { error: I18n.t("collavre.comments.move_not_allowed") }, status: :forbidden and return
|
|
386
422
|
end
|
|
@@ -398,20 +434,34 @@ module Collavre
|
|
|
398
434
|
render json: { error: I18n.t("collavre.comments.move_not_allowed") }, status: :forbidden and return
|
|
399
435
|
end
|
|
400
436
|
|
|
437
|
+
moved_count = 0
|
|
401
438
|
ActiveRecord::Base.transaction do
|
|
402
439
|
comments.each do |comment|
|
|
403
|
-
|
|
440
|
+
# Skip if already in target location
|
|
441
|
+
same_creative = comment.creative_id == target_origin.id
|
|
442
|
+
same_topic = comment.topic_id.to_s == new_topic_id.to_s
|
|
443
|
+
|
|
444
|
+
next if same_creative && same_topic
|
|
404
445
|
|
|
405
446
|
original_creative = comment.creative
|
|
406
|
-
comment.
|
|
407
|
-
|
|
447
|
+
original_topic_id = comment.topic_id
|
|
448
|
+
|
|
449
|
+
if same_creative
|
|
450
|
+
# Just update topic within same creative
|
|
451
|
+
comment.update!(topic_id: new_topic_id)
|
|
452
|
+
else
|
|
453
|
+
# Move to different creative
|
|
454
|
+
comment.update!(creative: target_origin, topic_id: new_topic_id)
|
|
455
|
+
broadcast_move_removal(comment, original_creative)
|
|
456
|
+
end
|
|
457
|
+
moved_count += 1
|
|
408
458
|
end
|
|
409
459
|
end
|
|
410
460
|
|
|
411
461
|
Comment.broadcast_badges(@creative)
|
|
412
462
|
Comment.broadcast_badges(target_origin) unless target_origin == @creative
|
|
413
463
|
|
|
414
|
-
render json: { success: true }
|
|
464
|
+
render json: { success: true, moved_count: moved_count }
|
|
415
465
|
rescue ActiveRecord::RecordInvalid => e
|
|
416
466
|
render json: { error: e.record.errors.full_messages.to_sentence.presence || I18n.t("collavre.comments.move_error") }, status: :unprocessable_entity
|
|
417
467
|
end
|
|
@@ -422,6 +472,57 @@ module Collavre
|
|
|
422
472
|
@creative = Creative.find(params[:creative_id]).effective_origin
|
|
423
473
|
end
|
|
424
474
|
|
|
475
|
+
def command_menu_items
|
|
476
|
+
[
|
|
477
|
+
{
|
|
478
|
+
name: "calendar",
|
|
479
|
+
label: "/calendar",
|
|
480
|
+
aliases: [ "/cal" ],
|
|
481
|
+
description: I18n.t("collavre.comments.command_menu.calendar_description"),
|
|
482
|
+
args: I18n.t("collavre.comments.command_menu.calendar_args")
|
|
483
|
+
}
|
|
484
|
+
] + mcp_command_items
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def mcp_command_items
|
|
488
|
+
Collavre::McpService.available_tools(Current.user).filter_map do |tool|
|
|
489
|
+
tool_name = tool[:name] || tool["name"]
|
|
490
|
+
next unless tool_name
|
|
491
|
+
|
|
492
|
+
{
|
|
493
|
+
name: tool_name,
|
|
494
|
+
label: "/#{tool_name}",
|
|
495
|
+
description: tool[:description] || tool["description"],
|
|
496
|
+
args: format_command_args(tool[:params] || tool["params"])
|
|
497
|
+
}
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def format_command_args(params)
|
|
502
|
+
return if params.blank?
|
|
503
|
+
|
|
504
|
+
# Handle array format (from MetaToolService)
|
|
505
|
+
if params.is_a?(Array)
|
|
506
|
+
return params.map do |param|
|
|
507
|
+
name = param[:name] || param["name"]
|
|
508
|
+
required = param[:required] || param["required"]
|
|
509
|
+
name.to_s + (required ? "*" : "")
|
|
510
|
+
end.join(", ")
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Handle JSON Schema format (Hash with :properties)
|
|
514
|
+
properties = params[:properties] || params["properties"]
|
|
515
|
+
return unless properties.is_a?(Hash)
|
|
516
|
+
|
|
517
|
+
required = params[:required] || params["required"] || []
|
|
518
|
+
required = Array(required).map(&:to_s)
|
|
519
|
+
|
|
520
|
+
properties.keys.map do |key|
|
|
521
|
+
key = key.to_s
|
|
522
|
+
required.include?(key) ? "#{key}*" : key
|
|
523
|
+
end.join(", ")
|
|
524
|
+
end
|
|
525
|
+
|
|
425
526
|
def set_comment
|
|
426
527
|
@comment = @creative.comments
|
|
427
528
|
.where(
|
|
@@ -236,6 +236,8 @@ module Collavre
|
|
|
236
236
|
permitted = creative_params.to_h
|
|
237
237
|
base = @creative.effective_origin(Set.new)
|
|
238
238
|
success = true
|
|
239
|
+
previous_progress = base.progress
|
|
240
|
+
requested_progress = permitted["progress"] || permitted[:progress]
|
|
239
241
|
|
|
240
242
|
# Handle parent_id change separately for Linked Creatives
|
|
241
243
|
if @creative.origin_id.present? && permitted.key?("parent_id")
|
|
@@ -251,6 +253,12 @@ module Collavre
|
|
|
251
253
|
permitted.delete(:origin_id)
|
|
252
254
|
|
|
253
255
|
success &&= base.update(permitted)
|
|
256
|
+
if success && requested_progress.present? && requested_progress.to_f >= 1 && previous_progress.to_f < 1
|
|
257
|
+
if base.children.exists?
|
|
258
|
+
base.self_and_descendants.where(origin_id: nil)
|
|
259
|
+
.update_all(progress: 1.0, updated_at: Time.current)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
254
262
|
|
|
255
263
|
if success
|
|
256
264
|
format.html { redirect_to @creative }
|
|
@@ -21,7 +21,11 @@ module Collavre
|
|
|
21
21
|
# for personal google service (like google calendar)
|
|
22
22
|
user.google_uid = auth.uid
|
|
23
23
|
user.google_access_token = auth.credentials.token
|
|
24
|
-
|
|
24
|
+
# Only update refresh_token if Google provides a new one
|
|
25
|
+
# (Google doesn't always return refresh_token on re-authentication)
|
|
26
|
+
if auth.credentials.refresh_token.present?
|
|
27
|
+
user.google_refresh_token = auth.credentials.refresh_token
|
|
28
|
+
end
|
|
25
29
|
user.google_token_expires_at = Time.at(auth.credentials.expires_at) if auth.credentials.expires_at
|
|
26
30
|
|
|
27
31
|
user.save! if user.new_record? || user.changed?
|
|
@@ -8,9 +8,9 @@ module Collavre
|
|
|
8
8
|
end
|
|
9
9
|
start_date = center - 30
|
|
10
10
|
end_date = center + 30
|
|
11
|
-
@plans = Plan.
|
|
12
|
-
.where("target_date >= ? AND created_at <= ?", start_date, end_date)
|
|
13
|
-
.order(
|
|
11
|
+
@plans = Plan.joins(:creative)
|
|
12
|
+
.where("target_date >= ? AND DATE(creatives.created_at) <= ?", start_date, end_date)
|
|
13
|
+
.order(Arel.sql("DATE(creatives.created_at) ASC"))
|
|
14
14
|
.select { |plan| plan.readable_by?(Current.user) }
|
|
15
15
|
calendar_scope = CalendarEvent.includes(:creative)
|
|
16
16
|
.where("DATE(start_time) <= ? AND DATE(end_time) >= ?", end_date, start_date)
|
|
@@ -68,25 +68,79 @@ module Collavre
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
def update
|
|
72
|
+
@plan = Plan.find(params[:id])
|
|
73
|
+
return render_forbidden unless plan_editable_by_current_user?
|
|
74
|
+
|
|
75
|
+
if @plan.update(plan_update_params)
|
|
76
|
+
respond_to do |format|
|
|
77
|
+
format.html do
|
|
78
|
+
redirect_back fallback_location: main_app.root_path,
|
|
79
|
+
notice: t("collavre.plans.updated", default: "Plan updated.")
|
|
80
|
+
end
|
|
81
|
+
format.json do
|
|
82
|
+
render json: plan_json(@plan, creative_id: params[:creative_id] || @plan.creative_id), status: :ok
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
respond_to do |format|
|
|
87
|
+
format.html do
|
|
88
|
+
flash[:alert] = @plan.errors.full_messages.join(", ")
|
|
89
|
+
redirect_back fallback_location: main_app.root_path
|
|
90
|
+
end
|
|
91
|
+
format.json do
|
|
92
|
+
render json: { errors: @plan.errors.full_messages }, status: :unprocessable_entity
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
71
98
|
private
|
|
72
99
|
|
|
73
100
|
def plan_params
|
|
74
|
-
params.require(:plan).permit(:target_date, :creative_id)
|
|
101
|
+
params.require(:plan).permit(:target_date, :start_date, :creative_id)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def plan_update_params
|
|
105
|
+
params.require(:plan).permit(:target_date, :start_date)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def plan_editable_by_current_user?
|
|
109
|
+
return true if @plan.owner_id == Current.user&.id
|
|
110
|
+
return true if @plan.creative&.has_permission?(Current.user, :write)
|
|
111
|
+
|
|
112
|
+
tagged_creative = Creative.find_by(id: params[:creative_id])
|
|
113
|
+
return false unless tagged_creative
|
|
114
|
+
return false unless @plan.tags.exists?(creative_id: tagged_creative.id)
|
|
115
|
+
|
|
116
|
+
tagged_creative.has_permission?(Current.user, :write)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def render_forbidden
|
|
120
|
+
respond_to do |format|
|
|
121
|
+
format.html do
|
|
122
|
+
redirect_back fallback_location: main_app.root_path,
|
|
123
|
+
alert: t("collavre.plans.update_forbidden", default: "You do not have permission to update this plan.")
|
|
124
|
+
end
|
|
125
|
+
format.json do
|
|
126
|
+
render json: { error: "forbidden" }, status: :forbidden
|
|
127
|
+
end
|
|
128
|
+
end
|
|
75
129
|
end
|
|
76
130
|
|
|
77
|
-
def plan_json(plan)
|
|
131
|
+
def plan_json(plan, creative_id: nil)
|
|
78
132
|
{
|
|
79
133
|
id: plan.id,
|
|
80
134
|
name: (plan.creative&.effective_description(nil, false) || plan.name.presence || I18n.l(plan.target_date)),
|
|
81
135
|
created_at: plan.created_at.to_date,
|
|
136
|
+
start_date: plan.start_date,
|
|
82
137
|
target_date: plan.target_date,
|
|
83
138
|
progress: plan.progress,
|
|
84
|
-
path: plan_creatives_path(plan),
|
|
139
|
+
path: plan_creatives_path(plan, creative_id: creative_id),
|
|
85
140
|
deletable: plan.owner_id == Current.user&.id
|
|
86
141
|
}
|
|
87
142
|
end
|
|
88
143
|
|
|
89
|
-
|
|
90
144
|
def calendar_json(event)
|
|
91
145
|
{
|
|
92
146
|
id: "calendar_event_#{event.id}",
|
|
@@ -99,8 +153,10 @@ module Collavre
|
|
|
99
153
|
}
|
|
100
154
|
end
|
|
101
155
|
|
|
102
|
-
def plan_creatives_path(plan)
|
|
103
|
-
if
|
|
156
|
+
def plan_creatives_path(plan, creative_id: nil)
|
|
157
|
+
if creative_id.present?
|
|
158
|
+
creative_path(creative_id, tags: [ plan.id ])
|
|
159
|
+
elsif params[:id].present?
|
|
104
160
|
creative_path(params[:id], tags: [ plan.id ])
|
|
105
161
|
else
|
|
106
162
|
creatives_path(tags: [ plan.id ])
|
|
@@ -29,6 +29,24 @@ module Collavre
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
def update
|
|
33
|
+
unless @creative.has_permission?(Current.user, :admin) || @creative.user == Current.user
|
|
34
|
+
render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
topic = @creative.topics.find(params[:id])
|
|
38
|
+
|
|
39
|
+
if topic.update(topic_params)
|
|
40
|
+
TopicsChannel.broadcast_to(
|
|
41
|
+
@creative,
|
|
42
|
+
{ action: "updated", topic: topic.slice(:id, :name) }
|
|
43
|
+
)
|
|
44
|
+
render json: topic
|
|
45
|
+
else
|
|
46
|
+
render json: { errors: topic.errors.full_messages }, status: :unprocessable_entity
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
32
50
|
def destroy
|
|
33
51
|
unless @creative.has_permission?(Current.user, :admin) || @creative.user == Current.user
|
|
34
52
|
render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return
|
|
@@ -45,6 +63,30 @@ module Collavre
|
|
|
45
63
|
head :no_content
|
|
46
64
|
end
|
|
47
65
|
|
|
66
|
+
def reorder
|
|
67
|
+
unless @creative.has_permission?(Current.user, :admin) || @creative.user == Current.user
|
|
68
|
+
render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
topic_ids = params[:topic_ids]
|
|
72
|
+
unless topic_ids.is_a?(Array) && topic_ids.present?
|
|
73
|
+
render json: { error: "Invalid topic_ids" }, status: :unprocessable_entity and return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
Topic.transaction do
|
|
77
|
+
topic_ids.each_with_index do |id, index|
|
|
78
|
+
@creative.topics.where(id: id).update_all(position: index)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
TopicsChannel.broadcast_to(
|
|
83
|
+
@creative,
|
|
84
|
+
{ action: "reordered", topic_ids: topic_ids }
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
render json: { success: true }
|
|
88
|
+
end
|
|
89
|
+
|
|
48
90
|
private
|
|
49
91
|
|
|
50
92
|
def set_creative
|
|
@@ -65,9 +65,10 @@ module Collavre
|
|
|
65
65
|
email: email,
|
|
66
66
|
password: SecureRandom.hex(36),
|
|
67
67
|
system_prompt: params[:system_prompt],
|
|
68
|
-
llm_vendor: "google",
|
|
68
|
+
llm_vendor: params[:llm_vendor].presence || "google",
|
|
69
69
|
llm_model: params[:llm_model],
|
|
70
70
|
llm_api_key: params[:llm_api_key],
|
|
71
|
+
gateway_url: params[:gateway_url],
|
|
71
72
|
tools: params[:tools] || [],
|
|
72
73
|
searchable: searchable,
|
|
73
74
|
email_verified_at: Time.current,
|
|
@@ -111,7 +112,7 @@ module Collavre
|
|
|
111
112
|
return
|
|
112
113
|
end
|
|
113
114
|
|
|
114
|
-
ai_params = params.require(:user).permit(:name, :system_prompt, :llm_model, :llm_api_key, :searchable, :routing_expression, tools: [])
|
|
115
|
+
ai_params = params.require(:user).permit(:name, :system_prompt, :llm_vendor, :llm_model, :llm_api_key, :gateway_url, :searchable, :routing_expression, tools: [])
|
|
115
116
|
|
|
116
117
|
if @user.update(ai_params)
|
|
117
118
|
redirect_to edit_ai_user_path(@user), notice: I18n.t("collavre.users.update_ai.success")
|
|
@@ -267,24 +268,13 @@ module Collavre
|
|
|
267
268
|
private
|
|
268
269
|
|
|
269
270
|
def load_available_tools
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
RailsMcpEngine::Engine.build_tools!
|
|
273
|
-
tools = ::Tools::MetaToolService.new.call(action: "list", tool_name: nil, query: nil, arguments: nil)
|
|
274
|
-
|
|
275
|
-
tool_list = Array(tools[:tools])
|
|
276
|
-
filtered_tools = Collavre::McpService.filter_tools(tool_list, Current.user)
|
|
277
|
-
|
|
278
|
-
filtered_tools.map do |tool|
|
|
271
|
+
Collavre::McpService.available_tools(Current.user).map do |tool|
|
|
279
272
|
{
|
|
280
273
|
name: tool[:name],
|
|
281
274
|
description: tool[:description],
|
|
282
275
|
parameters: tool[:params]
|
|
283
276
|
}
|
|
284
277
|
end
|
|
285
|
-
rescue StandardError => e
|
|
286
|
-
Rails.logger.error("Failed to load MCP tools: #{e.message}")
|
|
287
|
-
[]
|
|
288
278
|
end
|
|
289
279
|
|
|
290
280
|
def prepare_contacts
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class ApprovalPendingError < StandardError
|
|
5
|
+
attr_reader :tool_call, :task
|
|
6
|
+
|
|
7
|
+
def initialize(message = "Tool execution requires approval", tool_call: nil, task: nil)
|
|
8
|
+
@tool_call = tool_call
|
|
9
|
+
@task = task
|
|
10
|
+
super(message)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tool_name
|
|
14
|
+
return nil unless tool_call
|
|
15
|
+
|
|
16
|
+
if tool_call.respond_to?(:name)
|
|
17
|
+
tool_call.name
|
|
18
|
+
elsif tool_call.is_a?(Hash)
|
|
19
|
+
tool_call["name"] || tool_call[:name]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tool_arguments
|
|
24
|
+
return {} unless tool_call
|
|
25
|
+
|
|
26
|
+
if tool_call.respond_to?(:arguments)
|
|
27
|
+
tool_call.arguments
|
|
28
|
+
elsif tool_call.is_a?(Hash)
|
|
29
|
+
tool_call["arguments"] || tool_call[:arguments] || {}
|
|
30
|
+
else
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tool_call_id
|
|
36
|
+
return nil unless tool_call
|
|
37
|
+
|
|
38
|
+
if tool_call.respond_to?(:id)
|
|
39
|
+
tool_call.id
|
|
40
|
+
elsif tool_call.is_a?(Hash)
|
|
41
|
+
tool_call["id"] || tool_call[:id]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
tool_name: tool_name,
|
|
48
|
+
tool_call_id: tool_call_id,
|
|
49
|
+
arguments: tool_arguments,
|
|
50
|
+
task_id: task&.id
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -40,6 +40,8 @@ module Collavre
|
|
|
40
40
|
render_nav_divider(item)
|
|
41
41
|
when :raw
|
|
42
42
|
render_nav_raw(item)
|
|
43
|
+
when :popup
|
|
44
|
+
render_nav_dropdown(item, mobile: mobile)
|
|
43
45
|
else
|
|
44
46
|
raise ArgumentError, "Unknown navigation item type: #{item[:type]}"
|
|
45
47
|
end
|
|
@@ -123,7 +125,7 @@ module Collavre
|
|
|
123
125
|
def render_dropdown_child(item, mobile: false)
|
|
124
126
|
content = render_navigation_item(item, mobile: mobile)
|
|
125
127
|
return if content.blank?
|
|
126
|
-
content_tag(:div, content)
|
|
128
|
+
content_tag(:div, content, class: "popup-menu-item")
|
|
127
129
|
end
|
|
128
130
|
|
|
129
131
|
def resolve_nav_label(label)
|
data/app/javascript/collavre.js
CHANGED
|
@@ -6,6 +6,7 @@ import "./modules/creatives"
|
|
|
6
6
|
import "./modules/plans_timeline"
|
|
7
7
|
import "./modules/creative_row_swipe"
|
|
8
8
|
import "./modules/mention_menu"
|
|
9
|
+
import "./modules/command_menu"
|
|
9
10
|
import "./modules/export_to_markdown"
|
|
10
11
|
import "./modules/plans_menu"
|
|
11
12
|
import "./modules/inbox_panel"
|
|
@@ -14,9 +14,10 @@ describe('CommentsPopupController', () => {
|
|
|
14
14
|
beforeEach(() => {
|
|
15
15
|
container = document.createElement('div')
|
|
16
16
|
container.innerHTML = `
|
|
17
|
-
<div id="comments-popup" data-controller="comments--popup" style="width: 300px; height: 400px; position: absolute;">
|
|
17
|
+
<div id="comments-popup" data-controller="comments--popup" data-fullscreen-url-template="/creatives/__CREATIVE_ID__/comments/fullscreen" style="width: 300px; height: 400px; position: absolute;">
|
|
18
18
|
<h3 data-comments--popup-target="title">Title</h3>
|
|
19
19
|
<div data-comments--popup-target="list">List</div>
|
|
20
|
+
<a data-comments--popup-target="fullscreenLink" href="#"></a>
|
|
20
21
|
<button data-comments--popup-target="closeButton">Close</button>
|
|
21
22
|
<div data-comments--popup-target="leftHandle"></div>
|
|
22
23
|
<div data-comments--popup-target="rightHandle"></div>
|
|
@@ -79,7 +79,8 @@ export default class extends Controller {
|
|
|
79
79
|
|
|
80
80
|
isMentionMenuVisible() {
|
|
81
81
|
const menu = document.getElementById('mention-menu')
|
|
82
|
-
|
|
82
|
+
const commandMenu = document.getElementById('command-menu')
|
|
83
|
+
return menu?.style.display === 'block' || commandMenu?.style.display === 'block'
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
disconnect() {
|