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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/comment_versions.css +76 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
- data/app/assets/stylesheets/collavre/creatives.css +73 -1
- data/app/assets/stylesheets/collavre/org_chart.css +319 -0
- data/app/assets/stylesheets/collavre/popup.css +68 -1
- data/app/controllers/collavre/application_controller.rb +13 -0
- data/app/controllers/collavre/comments/versions_controller.rb +82 -0
- data/app/controllers/collavre/comments_controller.rb +14 -153
- data/app/controllers/collavre/concerns/exportable.rb +30 -0
- data/app/controllers/collavre/concerns/shareable.rb +28 -0
- data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
- data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
- data/app/controllers/collavre/creative_imports_controller.rb +6 -0
- data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
- data/app/controllers/collavre/creative_plans_controller.rb +1 -1
- data/app/controllers/collavre/creative_shares_controller.rb +84 -14
- data/app/controllers/collavre/creatives_controller.rb +70 -194
- data/app/controllers/collavre/google_auth_controller.rb +3 -0
- data/app/controllers/collavre/invites_controller.rb +2 -1
- data/app/controllers/collavre/sessions_controller.rb +3 -0
- data/app/controllers/collavre/topics_controller.rb +39 -2
- data/app/controllers/collavre/users_controller.rb +5 -404
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
- data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
- data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
- data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
- data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +12 -9
- data/app/helpers/collavre/navigation_helper.rb +1 -1
- data/app/javascript/collavre.js +0 -1
- data/app/javascript/controllers/comment_controller.js +33 -70
- data/app/javascript/controllers/comment_version_controller.js +164 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
- data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
- data/app/javascript/controllers/comments/contexts_controller.js +363 -0
- data/app/javascript/controllers/comments/form_controller.js +304 -13
- data/app/javascript/controllers/comments/list_controller.js +151 -62
- data/app/javascript/controllers/comments/popup_controller.js +66 -38
- data/app/javascript/controllers/comments/presence_controller.js +2 -10
- data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
- data/app/javascript/controllers/comments/topics_controller.js +34 -10
- data/app/javascript/controllers/index.js +15 -1
- data/app/javascript/controllers/org_chart_controller.js +46 -0
- data/app/javascript/controllers/share_modal_controller.js +369 -0
- data/app/javascript/controllers/topic_search_controller.js +103 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
- data/app/javascript/lib/api/creatives.js +12 -0
- data/app/javascript/lib/api/csrf_fetch.js +35 -0
- data/app/javascript/lib/api/drag_drop.js +17 -0
- data/app/javascript/modules/command_menu.js +40 -0
- data/app/javascript/modules/creative_row_editor.js +88 -0
- data/app/javascript/modules/slide_view.js +2 -1
- data/app/jobs/collavre/ai_agent_job.rb +42 -30
- data/app/jobs/collavre/compress_job.rb +92 -0
- data/app/models/collavre/comment.rb +36 -1
- data/app/models/collavre/comment_version.rb +15 -0
- data/app/models/collavre/creative/describable.rb +1 -1
- data/app/models/collavre/creative.rb +51 -0
- data/app/models/collavre/task.rb +30 -2
- data/app/models/collavre/user.rb +20 -3
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
- data/app/services/collavre/ai_agent/message_builder.rb +85 -6
- data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
- data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
- data/app/services/collavre/ai_agent/review_handler.rb +18 -1
- data/app/services/collavre/ai_agent_service.rb +130 -183
- data/app/services/collavre/ai_client.rb +6 -0
- data/app/services/collavre/auto_theme_generator.rb +1 -1
- data/app/services/collavre/command_menu_service.rb +19 -0
- data/app/services/collavre/comments/command_processor.rb +3 -1
- data/app/services/collavre/comments/compress_command.rb +75 -0
- data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
- data/app/services/collavre/comments/work_command.rb +161 -0
- data/app/services/collavre/comments/workflow_executor.rb +276 -0
- data/app/services/collavre/creatives/plan_tagger.rb +14 -3
- data/app/services/collavre/creatives/tree_formatter.rb +53 -13
- data/app/services/collavre/gemini_parent_recommender.rb +4 -4
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
- data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
- data/app/services/collavre/orchestration/scheduler.rb +3 -2
- data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
- data/app/services/collavre/system_events/dispatcher.rb +9 -0
- data/app/services/collavre/tools/creative_create_service.rb +1 -8
- data/app/services/collavre/tools/creative_import_service.rb +46 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
- data/app/services/collavre/tools/creative_update_service.rb +1 -8
- data/app/services/collavre/tools/cron_list_service.rb +1 -1
- data/app/services/collavre/tools/description_normalizable.rb +16 -0
- data/app/views/collavre/comments/_comment.html.erb +25 -8
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
- data/app/views/collavre/creatives/_share_button.html.erb +4 -1
- data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
- data/app/views/collavre/creatives/index.html.erb +5 -5
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
- data/app/views/collavre/users/_org_chart.html.erb +68 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
- data/app/views/collavre/users/new_ai.html.erb +9 -0
- data/app/views/collavre/users/show.html.erb +32 -8
- data/config/locales/comments.en.yml +57 -2
- data/config/locales/comments.ko.yml +57 -2
- data/config/locales/contacts.en.yml +31 -0
- data/config/locales/contacts.ko.yml +31 -0
- data/config/locales/contexts.en.yml +8 -0
- data/config/locales/contexts.ko.yml +8 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +6 -0
- data/config/locales/users.en.yml +1 -0
- data/config/locales/users.ko.yml +1 -0
- data/config/routes.rb +14 -1
- data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
- data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
- data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
- data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
- data/lib/collavre/version.rb +1 -1
- metadata +47 -10
- data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
- data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
- data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
- data/app/javascript/modules/share_modal.js +0 -76
- data/app/javascript/modules/share_user_popup.js +0 -77
- data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
- data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
- 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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|