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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comments_popup.css +293 -8
  3. data/app/assets/stylesheets/collavre/mention_menu.css +26 -0
  4. data/app/assets/stylesheets/collavre/popup.css +7 -0
  5. data/app/assets/stylesheets/collavre/print.css +18 -0
  6. data/app/channels/collavre/comments_presence_channel.rb +33 -0
  7. data/app/components/collavre/autocomplete_popup_component.html.erb +3 -0
  8. data/app/components/collavre/autocomplete_popup_component.rb +18 -0
  9. data/app/components/collavre/command_menu_component.rb +7 -0
  10. data/app/components/collavre/plans_timeline_component.html.erb +1 -1
  11. data/app/components/collavre/plans_timeline_component.rb +29 -32
  12. data/app/components/collavre/user_mention_menu_component.rb +4 -5
  13. data/app/controllers/collavre/comments_controller.rb +111 -10
  14. data/app/controllers/collavre/creatives_controller.rb +8 -0
  15. data/app/controllers/collavre/google_auth_controller.rb +5 -1
  16. data/app/controllers/collavre/plans_controller.rb +65 -9
  17. data/app/controllers/collavre/topics_controller.rb +42 -0
  18. data/app/controllers/collavre/users_controller.rb +4 -14
  19. data/app/errors/collavre/approval_pending_error.rb +54 -0
  20. data/app/errors/collavre/cancelled_error.rb +9 -0
  21. data/app/helpers/collavre/navigation_helper.rb +3 -1
  22. data/app/javascript/collavre.js +1 -0
  23. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +2 -1
  24. data/app/javascript/controllers/comments/form_controller.js +2 -1
  25. data/app/javascript/controllers/comments/list_controller.js +185 -2
  26. data/app/javascript/controllers/comments/popup_controller.js +95 -20
  27. data/app/javascript/controllers/comments/presence_controller.js +30 -1
  28. data/app/javascript/controllers/comments/topics_controller.js +314 -4
  29. data/app/javascript/modules/__tests__/creative_progress.test.js +50 -0
  30. data/app/javascript/modules/command_menu.js +116 -0
  31. data/app/javascript/modules/creative_progress.js +14 -0
  32. data/app/javascript/modules/creative_row_editor.js +104 -20
  33. data/app/javascript/modules/plans_timeline.js +15 -4
  34. data/app/javascript/modules/share_modal.js +3 -0
  35. data/app/jobs/collavre/ai_agent_job.rb +35 -21
  36. data/app/models/collavre/calendar_event.rb +7 -1
  37. data/app/models/collavre/comment.rb +35 -2
  38. data/app/models/collavre/creative.rb +1 -3
  39. data/app/models/collavre/mcp_tool.rb +4 -0
  40. data/app/models/collavre/plan.rb +23 -0
  41. data/app/models/collavre/topic.rb +12 -0
  42. data/app/models/collavre/user.rb +15 -1
  43. data/app/services/collavre/ai_agent_service.rb +174 -66
  44. data/app/services/collavre/ai_client.rb +31 -2
  45. data/app/services/collavre/comments/action_executor.rb +47 -1
  46. data/app/services/collavre/comments/calendar_command.rb +117 -18
  47. data/app/services/collavre/google_calendar_service.rb +38 -15
  48. data/app/services/collavre/markdown_importer.rb +47 -8
  49. data/app/services/collavre/mcp_service.rb +23 -10
  50. data/app/services/collavre/system_events/router.rb +50 -26
  51. data/app/services/collavre/tools/creative_create_service.rb +97 -0
  52. data/app/services/collavre/tools/creative_update_service.rb +116 -0
  53. data/app/views/collavre/comments/_comment.html.erb +2 -2
  54. data/app/views/collavre/comments/_comments_popup.html.erb +40 -6
  55. data/app/views/collavre/comments/fullscreen.html.erb +5 -0
  56. data/app/views/collavre/creatives/_inline_edit_form.html.erb +11 -3
  57. data/app/views/collavre/creatives/_integration_modals.html.erb +6 -0
  58. data/app/views/collavre/creatives/_integration_triggers.html.erb +8 -0
  59. data/app/views/collavre/creatives/_integrations_menu.html.erb +12 -0
  60. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +13 -1
  61. data/app/views/collavre/creatives/_share_button.html.erb +1 -1
  62. data/app/views/collavre/creatives/index.html.erb +22 -4
  63. data/app/views/collavre/users/edit_ai.html.erb +15 -0
  64. data/app/views/collavre/users/new_ai.html.erb +15 -0
  65. data/app/views/layouts/collavre/chat.html.erb +46 -0
  66. data/config/locales/ai_agent.en.yml +15 -0
  67. data/config/locales/ai_agent.ko.yml +15 -0
  68. data/config/locales/comments.en.yml +15 -3
  69. data/config/locales/comments.ko.yml +15 -3
  70. data/config/locales/creatives.en.yml +3 -31
  71. data/config/locales/creatives.ko.yml +3 -27
  72. data/config/locales/plans.en.yml +4 -0
  73. data/config/locales/plans.ko.yml +4 -0
  74. data/config/locales/users.en.yml +3 -0
  75. data/config/locales/users.ko.yml +3 -0
  76. data/config/routes.rb +8 -3
  77. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +1 -1
  78. data/db/migrate/20260131100000_migrate_active_storage_attachment_record_types.rb +21 -0
  79. data/db/migrate/20260201100000_make_google_event_id_nullable.rb +5 -0
  80. data/lib/collavre/engine.rb +171 -6
  81. data/lib/collavre/integration_registry.rb +129 -0
  82. data/lib/collavre/version.rb +1 -1
  83. data/lib/collavre.rb +2 -0
  84. data/lib/navigation/registry.rb +130 -0
  85. metadata +22 -15
  86. data/app/components/collavre/user_mention_menu_component.html.erb +0 -3
  87. data/app/controllers/collavre/notion_auth_controller.rb +0 -25
  88. data/app/jobs/collavre/notion_export_job.rb +0 -30
  89. data/app/jobs/collavre/notion_sync_job.rb +0 -48
  90. data/app/models/collavre/notion_account.rb +0 -17
  91. data/app/models/collavre/notion_block_link.rb +0 -10
  92. data/app/models/collavre/notion_page_link.rb +0 -19
  93. data/app/services/collavre/notion_client.rb +0 -231
  94. data/app/services/collavre/notion_creative_exporter.rb +0 -296
  95. data/app/services/collavre/notion_service.rb +0 -249
  96. data/app/views/collavre/creatives/_notion_integration_modal.html.erb +0 -90
  97. data/db/migrate/20241201000000_create_notion_integrations.rb +0 -29
  98. data/db/migrate/20250312000000_create_notion_block_links.rb +0 -16
  99. 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
- data = users.map do |u|
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: data
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
- target_creative = Creative.find_by(id: params[:target_creative_id])
378
- if target_creative.nil?
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
- next if comment.creative_id == target_origin.id
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.update!(creative: target_origin, topic_id: nil)
407
- broadcast_move_removal(comment, original_creative)
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
- user.google_refresh_token = auth.credentials.refresh_token || user.google_refresh_token
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.includes(:creative)
12
- .where("target_date >= ? AND created_at <= ?", start_date, end_date)
13
- .order(:created_at)
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 params[:id].present?
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
- return [] unless defined?(RailsMcpEngine)
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ class CancelledError < StandardError
5
+ def initialize(message = "Task was cancelled")
6
+ super(message)
7
+ end
8
+ end
9
+ 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)
@@ -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
- return menu?.style.display === 'block'
82
+ const commandMenu = document.getElementById('command-menu')
83
+ return menu?.style.display === 'block' || commandMenu?.style.display === 'block'
83
84
  }
84
85
 
85
86
  disconnect() {