collavre 0.5.0 → 0.7.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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comment_versions.css +76 -0
  3. data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
  4. data/app/assets/stylesheets/collavre/creatives.css +73 -1
  5. data/app/assets/stylesheets/collavre/org_chart.css +319 -0
  6. data/app/assets/stylesheets/collavre/popup.css +68 -1
  7. data/app/controllers/collavre/application_controller.rb +13 -0
  8. data/app/controllers/collavre/comments/versions_controller.rb +82 -0
  9. data/app/controllers/collavre/comments_controller.rb +14 -153
  10. data/app/controllers/collavre/concerns/exportable.rb +30 -0
  11. data/app/controllers/collavre/concerns/shareable.rb +28 -0
  12. data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
  13. data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
  14. data/app/controllers/collavre/creative_imports_controller.rb +6 -0
  15. data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
  16. data/app/controllers/collavre/creative_plans_controller.rb +1 -1
  17. data/app/controllers/collavre/creative_shares_controller.rb +84 -14
  18. data/app/controllers/collavre/creatives_controller.rb +70 -194
  19. data/app/controllers/collavre/google_auth_controller.rb +3 -0
  20. data/app/controllers/collavre/invites_controller.rb +2 -1
  21. data/app/controllers/collavre/sessions_controller.rb +3 -0
  22. data/app/controllers/collavre/topics_controller.rb +39 -2
  23. data/app/controllers/collavre/users_controller.rb +5 -404
  24. data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
  25. data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
  26. data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
  27. data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
  28. data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
  29. data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
  30. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
  31. data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
  32. data/app/helpers/collavre/application_helper.rb +1 -0
  33. data/app/helpers/collavre/creatives_helper.rb +12 -9
  34. data/app/helpers/collavre/navigation_helper.rb +1 -1
  35. data/app/javascript/collavre.js +0 -1
  36. data/app/javascript/controllers/comment_controller.js +33 -70
  37. data/app/javascript/controllers/comment_version_controller.js +164 -0
  38. data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
  39. data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
  40. data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
  41. data/app/javascript/controllers/comments/contexts_controller.js +363 -0
  42. data/app/javascript/controllers/comments/form_controller.js +304 -13
  43. data/app/javascript/controllers/comments/list_controller.js +151 -62
  44. data/app/javascript/controllers/comments/popup_controller.js +66 -38
  45. data/app/javascript/controllers/comments/presence_controller.js +2 -10
  46. data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
  47. data/app/javascript/controllers/comments/topics_controller.js +34 -10
  48. data/app/javascript/controllers/index.js +15 -1
  49. data/app/javascript/controllers/org_chart_controller.js +46 -0
  50. data/app/javascript/controllers/share_modal_controller.js +369 -0
  51. data/app/javascript/controllers/topic_search_controller.js +103 -0
  52. data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
  53. data/app/javascript/lib/api/creatives.js +12 -0
  54. data/app/javascript/lib/api/csrf_fetch.js +35 -0
  55. data/app/javascript/lib/api/drag_drop.js +17 -0
  56. data/app/javascript/modules/command_menu.js +40 -0
  57. data/app/javascript/modules/creative_row_editor.js +88 -0
  58. data/app/javascript/modules/slide_view.js +2 -1
  59. data/app/jobs/collavre/ai_agent_job.rb +42 -30
  60. data/app/jobs/collavre/compress_job.rb +92 -0
  61. data/app/models/collavre/comment.rb +36 -1
  62. data/app/models/collavre/comment_version.rb +15 -0
  63. data/app/models/collavre/creative/describable.rb +1 -1
  64. data/app/models/collavre/creative.rb +51 -0
  65. data/app/models/collavre/task.rb +30 -2
  66. data/app/models/collavre/user.rb +20 -3
  67. data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
  68. data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
  69. data/app/services/collavre/ai_agent/message_builder.rb +85 -6
  70. data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
  71. data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
  72. data/app/services/collavre/ai_agent/review_handler.rb +18 -1
  73. data/app/services/collavre/ai_agent_service.rb +130 -183
  74. data/app/services/collavre/ai_client.rb +6 -0
  75. data/app/services/collavre/auto_theme_generator.rb +1 -1
  76. data/app/services/collavre/command_menu_service.rb +19 -0
  77. data/app/services/collavre/comments/command_processor.rb +3 -1
  78. data/app/services/collavre/comments/compress_command.rb +75 -0
  79. data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
  80. data/app/services/collavre/comments/work_command.rb +161 -0
  81. data/app/services/collavre/comments/workflow_executor.rb +276 -0
  82. data/app/services/collavre/creatives/plan_tagger.rb +14 -3
  83. data/app/services/collavre/creatives/tree_formatter.rb +53 -13
  84. data/app/services/collavre/gemini_parent_recommender.rb +4 -4
  85. data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
  86. data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
  87. data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
  88. data/app/services/collavre/orchestration/scheduler.rb +3 -2
  89. data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
  90. data/app/services/collavre/system_events/dispatcher.rb +9 -0
  91. data/app/services/collavre/tools/creative_create_service.rb +1 -8
  92. data/app/services/collavre/tools/creative_import_service.rb +46 -0
  93. data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
  94. data/app/services/collavre/tools/creative_update_service.rb +1 -8
  95. data/app/services/collavre/tools/cron_list_service.rb +1 -1
  96. data/app/services/collavre/tools/description_normalizable.rb +16 -0
  97. data/app/views/collavre/comments/_comment.html.erb +25 -8
  98. data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
  99. data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
  100. data/app/views/collavre/creatives/_share_button.html.erb +4 -1
  101. data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
  102. data/app/views/collavre/creatives/index.html.erb +5 -5
  103. data/app/views/collavre/creatives/slide_view.html.erb +1 -1
  104. data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
  105. data/app/views/collavre/users/_org_chart.html.erb +68 -0
  106. data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
  107. data/app/views/collavre/users/new_ai.html.erb +9 -0
  108. data/app/views/collavre/users/show.html.erb +32 -8
  109. data/config/locales/comments.en.yml +57 -2
  110. data/config/locales/comments.ko.yml +57 -2
  111. data/config/locales/contacts.en.yml +31 -0
  112. data/config/locales/contacts.ko.yml +31 -0
  113. data/config/locales/contexts.en.yml +8 -0
  114. data/config/locales/contexts.ko.yml +8 -0
  115. data/config/locales/creatives.en.yml +6 -0
  116. data/config/locales/creatives.ko.yml +6 -0
  117. data/config/locales/users.en.yml +1 -0
  118. data/config/locales/users.ko.yml +1 -0
  119. data/config/routes.rb +14 -1
  120. data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
  121. data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
  122. data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
  123. data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
  124. data/lib/collavre/version.rb +1 -1
  125. metadata +47 -10
  126. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
  127. data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
  128. data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
  129. data/app/javascript/modules/share_modal.js +0 -76
  130. data/app/javascript/modules/share_user_popup.js +0 -77
  131. data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
  132. data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
  133. data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module AiAgent
5
+ # Manages agent lifecycle during task execution:
6
+ # - Status broadcasting (thinking, streaming, idle)
7
+ # - Cancellation checking
8
+ # - Heartbeat management
9
+ class AgentLifecycleManager
10
+ # Minimum interval (in seconds) between cancellation checks to avoid excessive DB queries
11
+ CANCEL_CHECK_INTERVAL = 1.0
12
+ # Interval (in seconds) between agent_status heartbeats during streaming
13
+ AGENT_STATUS_HEARTBEAT_INTERVAL = 3.0
14
+
15
+ def initialize(task:, agent:, creative:)
16
+ @task = task
17
+ @agent = agent
18
+ @creative = creative
19
+ @last_cancel_check_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ @last_heartbeat_at = @last_cancel_check_at
21
+ end
22
+
23
+ # Broadcast agent status change
24
+ def broadcast_status(status, content: nil)
25
+ return unless @creative
26
+
27
+ CommentsPresenceChannel.broadcast_agent_status(
28
+ @creative.effective_origin.id,
29
+ status: status,
30
+ agent_id: @agent.id,
31
+ agent_name: @agent.display_name,
32
+ task_id: @task.id,
33
+ content: content,
34
+ source_creative_id: @creative.id
35
+ )
36
+ end
37
+
38
+ # Check if task was cancelled, raise Collavre::CancelledError if so
39
+ def check_cancelled!
40
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
41
+ return if (now - @last_cancel_check_at) < CANCEL_CHECK_INTERVAL
42
+
43
+ @last_cancel_check_at = now
44
+ raise Collavre::CancelledError if @task.reload.status == "cancelled"
45
+ end
46
+
47
+ # Send heartbeat if interval passed
48
+ def heartbeat_if_needed
49
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
+ if (now - @last_heartbeat_at) >= AGENT_STATUS_HEARTBEAT_INTERVAL
51
+ broadcast_status("streaming")
52
+ @last_heartbeat_at = now
53
+ end
54
+ end
55
+
56
+ # Handle cancellation cleanup
57
+ def handle_cancelled(reply_comment:, response_content:)
58
+ if reply_comment
59
+ if response_content.present?
60
+ reply_comment.content_will_change!
61
+ reply_comment.update!(content: response_content)
62
+ reply_comment.broadcast_update_to(
63
+ [ reply_comment.creative, :comments ],
64
+ partial: "collavre/comments/comment",
65
+ locals: { comment: reply_comment, streaming: false }
66
+ )
67
+ log_action("reply_created", { comment_id: reply_comment.id, content: response_content, partial: true })
68
+ else
69
+ reply_comment.destroy!
70
+ end
71
+ end
72
+
73
+ broadcast_status("idle")
74
+ log_action("cancelled", { message: "Task cancelled by user" })
75
+ end
76
+
77
+ private
78
+
79
+ def log_action(type, payload, result = nil)
80
+ @task.task_actions.create!(
81
+ action_type: type,
82
+ payload: payload,
83
+ result: result,
84
+ status: "done"
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
@@ -15,8 +15,11 @@ module Collavre
15
15
 
16
16
  def build
17
17
  messages = []
18
+ @injected_creative_ids = Set.new
18
19
 
19
20
  append_creative_context(messages)
21
+ append_context_creatives(messages)
22
+ append_referenced_creative_contexts(messages)
20
23
  append_chat_history(messages)
21
24
  append_trigger_message(messages)
22
25
 
@@ -32,16 +35,92 @@ module Collavre
32
35
  creative = Creative.find_by(id: creative_id)
33
36
  return unless creative
34
37
 
38
+ effective = creative.effective_origin(Set.new)
39
+ topic = current_topic
40
+ topic_info = topic ? "\nTopic: #{topic.name} (id: #{topic.id})" : ""
41
+
42
+ if effective.data&.dig("disabled_self_context") == true
43
+ # Self-context disabled: inject only the ancestry chain so the AI
44
+ # knows where in the hierarchy the conversation is happening
45
+ ancestry = build_ancestry_chain(creative)
46
+ @injected_creative_ids << creative.id
47
+ messages << { role: "user", parts: [ { text: "Current Creative (id: #{creative.id}):#{topic_info}\nPath: #{ancestry}" } ] }
48
+ else
49
+ # Full self-context: inject the creative subtree
50
+ children_level = @agent.creative_children_level
51
+ max_depth = 1 + children_level
52
+ markdown = ApplicationController.helpers.render_creative_tree_markdown(
53
+ [ creative ], 1, true, max_depth: max_depth
54
+ )
55
+
56
+ @injected_creative_ids << creative.id
57
+ messages << { role: "user", parts: [ { text: "Creative (id: #{creative.id}):#{topic_info}\n#{markdown}" } ] }
58
+ end
59
+ end
60
+
61
+ def build_ancestry_chain(creative)
62
+ creative.self_and_ancestors.reverse.map(&:creative_snippet).join(" > ")
63
+ end
64
+
65
+ def append_context_creatives(messages)
66
+ creative_id = @context.dig("creative", "id")
67
+ return unless creative_id
68
+
69
+ creative = Creative.find_by(id: creative_id)
70
+ return unless creative
71
+
72
+ effective_origin = creative.effective_origin(Set.new)
73
+ context_ids = effective_origin.effective_context_ids
74
+ disabled_ids = Array(effective_origin.data&.dig("disabled_context_ids"))
75
+ active_ids = context_ids - disabled_ids - [ creative_id, effective_origin.id ]
76
+ return if active_ids.empty?
77
+
35
78
  children_level = @agent.creative_children_level
36
79
  max_depth = 1 + children_level
37
- markdown = ApplicationController.helpers.render_creative_tree_markdown(
38
- [ creative ], 1, true, max_depth: max_depth
39
- )
40
80
 
41
- topic = current_topic
42
- topic_info = topic ? "\nTopic: #{topic.name} (id: #{topic.id})" : ""
81
+ active_ids.each do |ctx_id|
82
+ next if @injected_creative_ids.include?(ctx_id)
83
+
84
+ ctx = Creative.find_by(id: ctx_id)
85
+ next unless ctx
86
+
87
+ @injected_creative_ids << ctx_id
88
+ markdown = ApplicationController.helpers.render_creative_tree_markdown(
89
+ [ ctx ], 1, true, max_depth: max_depth
90
+ )
91
+
92
+ messages << {
93
+ role: "user",
94
+ parts: [ { text: "Context Creative (id: #{ctx.id}):\n#{markdown}" } ]
95
+ }
96
+ end
97
+ end
98
+
99
+ def append_referenced_creative_contexts(messages)
100
+ content = @context.dig("comment", "content")
101
+ return unless content
43
102
 
44
- messages << { role: "user", parts: [ { text: "Creative (id: #{creative.id}):#{topic_info}\n#{markdown}" } ] }
103
+ children_level = @agent.creative_children_level
104
+ max_depth = 1 + children_level
105
+
106
+ # Extract creative IDs from markdown links like [title](/creatives/123)
107
+ content.scan(%r{\[[^\]]*\]\(/creatives/(\d+)\)}).flatten.uniq.each do |id_str|
108
+ creative_id = id_str.to_i
109
+ next if @injected_creative_ids.include?(creative_id)
110
+
111
+ creative = Creative.find_by(id: creative_id)
112
+ next unless creative
113
+
114
+ @injected_creative_ids << creative_id
115
+ markdown = ApplicationController.helpers.render_creative_tree_markdown(
116
+ [ creative ], 1, true, max_depth: max_depth
117
+ )
118
+
119
+ messages << {
120
+ role: "user",
121
+ parts: [ { text: "Referenced Creative (id: #{creative.id}):\n#{markdown}" } ]
122
+ }
123
+ end
45
124
  end
46
125
 
47
126
  def append_chat_history(messages)
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module AiAgent
5
+ # Handles finalization of AI agent responses:
6
+ # - Review workflow processing
7
+ # - Comment creation/update
8
+ # - Activity log reassociation
9
+ class ResponseFinalizer
10
+ def initialize(task:, agent:, original_comment:, reply_comment:, response_content:)
11
+ @task = task
12
+ @agent = agent
13
+ @original_comment = original_comment
14
+ @reply_comment = reply_comment
15
+ @response_content = response_content
16
+ end
17
+
18
+ # Whether the last finalization was a review-flow update (no new comment created)
19
+ attr_reader :review_flow
20
+
21
+ # Finalize the response, handling review workflow if applicable
22
+ # Returns the finalized comment or nil
23
+ def finalize
24
+ @review_flow = false
25
+ # If no content, destroy placeholder and return
26
+ if @response_content.blank?
27
+ @reply_comment&.destroy!
28
+ return nil
29
+ end
30
+
31
+ review_handler = ReviewHandler.new(@original_comment, @agent)
32
+
33
+ if @reply_comment
34
+ finalize_reply_comment(review_handler)
35
+ elsif @original_comment
36
+ create_reply_comment
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def finalize_reply_comment(review_handler)
43
+ if @original_comment&.review_message? && review_handler.handle(@response_content, task: @task)
44
+ # Review workflow: update quoted comment in place (no new comment created)
45
+ @review_flow = true
46
+ review_handler.add_completion_reaction
47
+ reassociate_activity_logs(@reply_comment, @original_comment.quoted_comment)
48
+ @reply_comment.destroy!
49
+ @original_comment.quoted_comment
50
+ else
51
+ # Normal comment workflow
52
+ update_reply_comment
53
+ reassociate_activity_logs(@original_comment, @reply_comment)
54
+ @reply_comment
55
+ end
56
+ end
57
+
58
+ def update_reply_comment
59
+ @reply_comment.content_will_change!
60
+ @reply_comment.update!(content: @response_content)
61
+ @reply_comment.broadcast_update_to(
62
+ [ @reply_comment.creative, :comments ],
63
+ partial: "collavre/comments/comment",
64
+ locals: { comment: @reply_comment, streaming: false }
65
+ )
66
+
67
+ log_action("reply_created", { comment_id: @reply_comment.id, content: @response_content })
68
+ end
69
+
70
+ def create_reply_comment
71
+ reply_comment = @original_comment.creative.comments.create!(
72
+ content: @response_content,
73
+ user: @agent,
74
+ topic_id: @original_comment.topic_id
75
+ )
76
+
77
+ log_action("reply_created", { comment_id: reply_comment.id, content: @response_content })
78
+ reassociate_activity_logs(@original_comment, reply_comment)
79
+ reply_comment
80
+ end
81
+
82
+ def reassociate_activity_logs(from_comment, to_comment)
83
+ ActivityLog.where(comment: from_comment, user: @agent)
84
+ .update_all(comment_id: to_comment.id)
85
+ end
86
+
87
+ def log_action(type, payload, result = nil)
88
+ @task.task_actions.create!(
89
+ action_type: type,
90
+ payload: payload,
91
+ result: result,
92
+ status: "done"
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module AiAgent
5
+ # Handles streaming response content from AI to comments.
6
+ # Manages throttling, broadcasting, and placeholder cleanup.
7
+ class ResponseStreamer
8
+ # Minimum interval (in seconds) between streaming updates to avoid excessive DB writes
9
+ STREAM_THROTTLE_INTERVAL = 0.1
10
+
11
+ def initialize(reply_comment:, creative:)
12
+ @reply_comment = reply_comment
13
+ @creative = creative
14
+ @content = ""
15
+ @last_broadcast_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ end
17
+
18
+ # Append delta content and broadcast if throttle interval passed
19
+ def append(delta)
20
+ @content += delta
21
+
22
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+ if @reply_comment && (now - @last_broadcast_at) >= STREAM_THROTTLE_INTERVAL
24
+ update_and_broadcast(streaming: true)
25
+ @last_broadcast_at = now
26
+ end
27
+ end
28
+
29
+ # Get accumulated content
30
+ def content
31
+ @content
32
+ end
33
+
34
+ # Finalize streaming by broadcasting final update
35
+ def finalize
36
+ update_and_broadcast(streaming: false) if @reply_comment && @content.present?
37
+ end
38
+
39
+ # Check if there's content accumulated
40
+ def content_present?
41
+ @content.present?
42
+ end
43
+
44
+ private
45
+
46
+ def update_and_broadcast(streaming:)
47
+ @reply_comment.update_column(:content, @content)
48
+ @reply_comment.broadcast_update_to(
49
+ [ @reply_comment.creative, :comments ],
50
+ partial: "collavre/comments/comment",
51
+ locals: { comment: @reply_comment, streaming: streaming }
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -34,7 +34,24 @@ module Collavre
34
34
  return false unless eligible?
35
35
 
36
36
  quoted_comment = @original_comment.quoted_comment
37
- quoted_comment.update!(content: response_content)
37
+
38
+ # Save old content as a version if this is the first review (no versions yet)
39
+ if quoted_comment.comment_versions.empty?
40
+ quoted_comment.comment_versions.create!(
41
+ content: quoted_comment.content,
42
+ version_number: quoted_comment.next_version_number
43
+ )
44
+ end
45
+
46
+ # Save new content as the latest version
47
+ new_version = quoted_comment.comment_versions.create!(
48
+ content: response_content,
49
+ version_number: quoted_comment.next_version_number,
50
+ review_comment: @original_comment
51
+ )
52
+
53
+ # Update content and point to the new version
54
+ quoted_comment.update!(content: response_content, selected_version_id: new_version.id)
38
55
 
39
56
  task.task_actions.create!(
40
57
  action_type: "review_updated",