collavre 0.3.2 → 0.4.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 +67 -0
- data/app/controllers/collavre/comments/reactions_controller.rb +1 -9
- data/app/controllers/collavre/comments_controller.rb +5 -4
- data/app/controllers/collavre/users_controller.rb +4 -1
- data/app/javascript/collavre.js +0 -1
- data/app/javascript/controllers/comment_controller.js +102 -0
- data/app/javascript/controllers/comments/form_controller.js +23 -0
- data/app/models/collavre/comment.rb +5 -0
- data/app/models/collavre/comment_reaction.rb +15 -0
- data/app/services/collavre/ai_agent_service.rb +77 -9
- data/app/services/collavre/orchestration/policy_resolver.rb +1 -1
- data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
- data/app/views/collavre/comments/_comment.html.erb +5 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +8 -1
- data/app/views/collavre/creatives/index.html.erb +0 -2
- data/config/locales/ai_agent.en.yml +2 -0
- data/config/locales/ai_agent.ko.yml +2 -0
- data/config/locales/comments.en.yml +1 -0
- data/config/locales/comments.ko.yml +1 -0
- data/db/migrate/20260212011655_add_quoted_comment_to_comments.rb +7 -0
- data/lib/collavre/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22776a6f9413458cb9ff5c5c9518b1291939beab2b387c6e1e4d7f557a8585c4
|
|
4
|
+
data.tar.gz: 0dde98de3d1d7c8dc983df5ebcb89d6eb9d9851d5e8c473214def3bcdad7c954
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 495868ccfef0655e1de79cddb77cb7321ce50cbb26774fa13f45bd55cc0d1f76dd61e6c426b1f88234fdb2167ff7b757b4ef07ee9a96b932b6fa742121a8c177
|
|
7
|
+
data.tar.gz: f2b130635ea439864cc07a6d4bac31b8a13a4b265659fbb1aae06450498d691c6fe3d9b55fbcd874cf23140716587bc16fd80d4c0214913aff0de7c0ff53d302
|
|
@@ -180,6 +180,73 @@ body.chat-fullscreen {
|
|
|
180
180
|
margin-top: 0.2em;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
.comment-quoted-block {
|
|
184
|
+
margin-top: 0.3em;
|
|
185
|
+
margin-bottom: 0.2em;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.comment-quoted-text {
|
|
189
|
+
border-left: 3px solid var(--color-border);
|
|
190
|
+
padding: 0.3em 0.6em;
|
|
191
|
+
margin: 0;
|
|
192
|
+
color: var(--color-muted);
|
|
193
|
+
font-size: 0.9em;
|
|
194
|
+
background: color-mix(in srgb, var(--color-section-bg) 50%, transparent);
|
|
195
|
+
border-radius: 0 4px 4px 0;
|
|
196
|
+
white-space: pre-wrap;
|
|
197
|
+
word-break: break-word;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Quote indicator in form */
|
|
201
|
+
.comment-quote-indicator {
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: space-between;
|
|
205
|
+
padding: 0.3em 0.6em;
|
|
206
|
+
margin-bottom: 0.3em;
|
|
207
|
+
background: color-mix(in srgb, var(--color-section-bg) 50%, transparent);
|
|
208
|
+
border-left: 3px solid var(--color-accent, #007bff);
|
|
209
|
+
border-radius: 0 4px 4px 0;
|
|
210
|
+
font-size: 0.85em;
|
|
211
|
+
color: var(--color-muted);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.comment-quote-indicator-text {
|
|
215
|
+
overflow: hidden;
|
|
216
|
+
text-overflow: ellipsis;
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
flex: 1;
|
|
219
|
+
min-width: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.comment-quote-cancel {
|
|
223
|
+
border: none;
|
|
224
|
+
background: none;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
color: var(--color-muted);
|
|
227
|
+
font-size: 1.1em;
|
|
228
|
+
padding: 0 0.2em;
|
|
229
|
+
line-height: 1;
|
|
230
|
+
flex-shrink: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.comment-quote-cancel:hover {
|
|
234
|
+
color: var(--color-text);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Review popup (uses common-popup) */
|
|
238
|
+
.comment-review-popup {
|
|
239
|
+
min-width: auto;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.comment-review-popup .common-popup-item {
|
|
243
|
+
padding: 0.3em 0.8em;
|
|
244
|
+
font-size: 0.85em;
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
border-radius: 4px;
|
|
247
|
+
white-space: nowrap;
|
|
248
|
+
}
|
|
249
|
+
|
|
183
250
|
.comment-reactions {
|
|
184
251
|
margin-top: 0.4em;
|
|
185
252
|
display: flex;
|
|
@@ -45,15 +45,7 @@ module Collavre
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def broadcast_reaction_update
|
|
48
|
-
|
|
49
|
-
Turbo::StreamsChannel.broadcast_action_to(
|
|
50
|
-
[ @creative, :comments ],
|
|
51
|
-
action: "update_reactions",
|
|
52
|
-
target: view_context.dom_id(@comment),
|
|
53
|
-
attributes: {
|
|
54
|
-
data: payload.to_json
|
|
55
|
-
}
|
|
56
|
-
)
|
|
48
|
+
CommentReaction.broadcast_reaction_update(@comment)
|
|
57
49
|
end
|
|
58
50
|
|
|
59
51
|
def set_creative
|
|
@@ -167,8 +167,9 @@ module Collavre
|
|
|
167
167
|
comment: {
|
|
168
168
|
id: @comment.id,
|
|
169
169
|
content: @comment.content,
|
|
170
|
-
user_id: @comment.user_id
|
|
171
|
-
|
|
170
|
+
user_id: @comment.user_id,
|
|
171
|
+
quoted_comment_id: @comment.quoted_comment_id
|
|
172
|
+
}.compact,
|
|
172
173
|
creative: {
|
|
173
174
|
id: @creative.id,
|
|
174
175
|
description: @creative.description
|
|
@@ -189,7 +190,7 @@ module Collavre
|
|
|
189
190
|
|
|
190
191
|
def update
|
|
191
192
|
if @comment.user == Current.user
|
|
192
|
-
safe_params = comment_params
|
|
193
|
+
safe_params = comment_params.except(:quoted_comment_id, :quoted_text)
|
|
193
194
|
if safe_params[:topic_id].present? && !@creative.topics.where(id: safe_params[:topic_id]).exists?
|
|
194
195
|
render json: { error: I18n.t("collavre.comments.invalid_topic") }, status: :unprocessable_entity and return
|
|
195
196
|
end
|
|
@@ -552,7 +553,7 @@ module Collavre
|
|
|
552
553
|
end
|
|
553
554
|
|
|
554
555
|
def comment_params
|
|
555
|
-
params.require(:comment).permit(:content, :private, :topic_id, images: [])
|
|
556
|
+
params.require(:comment).permit(:content, :private, :topic_id, :quoted_comment_id, :quoted_text, images: [])
|
|
556
557
|
end
|
|
557
558
|
|
|
558
559
|
def can_convert_comment?
|
|
@@ -137,7 +137,10 @@ module Collavre
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
scope = if params[:scope] == "contacts" && Current.user
|
|
140
|
-
|
|
140
|
+
# Include both contacts and searchable users (e.g., AI agents with searchable=true)
|
|
141
|
+
contact_ids = Current.user.contact_users.select(:id)
|
|
142
|
+
searchable_ids = Collavre::User.where(searchable: true).select(:id)
|
|
143
|
+
Collavre::User.where(id: contact_ids).or(Collavre::User.where(id: searchable_ids))
|
|
141
144
|
else
|
|
142
145
|
Collavre::User.mentionable_for(creative)
|
|
143
146
|
end
|
data/app/javascript/collavre.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
import { renderCommentMarkdown } from '../lib/utils/markdown'
|
|
3
|
+
import CommonPopup from '../lib/common_popup'
|
|
3
4
|
|
|
4
5
|
// Connects to data-controller="comment"
|
|
5
6
|
export default class extends Controller {
|
|
@@ -13,6 +14,10 @@ export default class extends Controller {
|
|
|
13
14
|
contentElement.dataset.rendered = 'true'
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
// Text selection quote support
|
|
18
|
+
this.handleMouseUp = this.handleMouseUp.bind(this)
|
|
19
|
+
this.element.addEventListener('mouseup', this.handleMouseUp)
|
|
20
|
+
|
|
16
21
|
this.currentUserId = document.body.dataset.currentUserId
|
|
17
22
|
const commentAuthorId = this.element.dataset.userId
|
|
18
23
|
const creativeOwnerId = this.element.dataset.creativeOwnerId
|
|
@@ -108,6 +113,103 @@ export default class extends Controller {
|
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
115
|
|
|
116
|
+
disconnect() {
|
|
117
|
+
this.element.removeEventListener('mouseup', this.handleMouseUp)
|
|
118
|
+
this.hideReviewPopup()
|
|
119
|
+
if (this._reviewPopupEl) {
|
|
120
|
+
this._reviewPopupEl.remove()
|
|
121
|
+
this._reviewPopupEl = null
|
|
122
|
+
this._reviewPopup = null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
handleMouseUp() {
|
|
127
|
+
// Small delay to let selection finalize
|
|
128
|
+
requestAnimationFrame(() => {
|
|
129
|
+
this.hideReviewPopup()
|
|
130
|
+
|
|
131
|
+
const selection = window.getSelection()
|
|
132
|
+
if (!selection || selection.isCollapsed) return
|
|
133
|
+
|
|
134
|
+
const selectedText = selection.toString().trim()
|
|
135
|
+
if (!selectedText) return
|
|
136
|
+
|
|
137
|
+
// Ensure selection is within this comment's content
|
|
138
|
+
const contentEl = this.element.querySelector('.comment-content')
|
|
139
|
+
if (!contentEl) return
|
|
140
|
+
|
|
141
|
+
const range = selection.getRangeAt(0)
|
|
142
|
+
if (!contentEl.contains(range.commonAncestorContainer)) return
|
|
143
|
+
|
|
144
|
+
// Show review popup below the selection using CommonPopup
|
|
145
|
+
const rect = range.getBoundingClientRect()
|
|
146
|
+
this.showReviewPopup(rect, selectedText)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
showReviewPopup(anchorRect, selectedText) {
|
|
151
|
+
// Create or reuse popup element
|
|
152
|
+
if (!this._reviewPopupEl) {
|
|
153
|
+
const el = document.createElement('div')
|
|
154
|
+
el.className = 'common-popup comment-review-popup'
|
|
155
|
+
el.style.display = 'none'
|
|
156
|
+
el.style.padding = '0.3em'
|
|
157
|
+
|
|
158
|
+
const list = document.createElement('ul')
|
|
159
|
+
list.style.listStyle = 'none'
|
|
160
|
+
list.style.margin = '0'
|
|
161
|
+
list.style.padding = '0'
|
|
162
|
+
el.appendChild(list)
|
|
163
|
+
|
|
164
|
+
const commentsPopup = this.element.closest('#comments-popup')
|
|
165
|
+
if (commentsPopup) {
|
|
166
|
+
commentsPopup.appendChild(el)
|
|
167
|
+
} else {
|
|
168
|
+
document.body.appendChild(el)
|
|
169
|
+
}
|
|
170
|
+
this._reviewPopupEl = el
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const reviewLabel = this.element.closest('#comments-popup')?.dataset?.reviewButtonText || 'Review'
|
|
174
|
+
|
|
175
|
+
this._reviewPopup = new CommonPopup(this._reviewPopupEl, {
|
|
176
|
+
onSelect: () => {
|
|
177
|
+
const commentId = this.element.dataset.commentId
|
|
178
|
+
const formController = this.findFormController()
|
|
179
|
+
if (formController) {
|
|
180
|
+
formController.quoteComment(commentId, selectedText)
|
|
181
|
+
}
|
|
182
|
+
window.getSelection().removeAllRanges()
|
|
183
|
+
this.hideReviewPopup()
|
|
184
|
+
},
|
|
185
|
+
renderItem: () => reviewLabel,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// Position below the selection end
|
|
189
|
+
const belowRect = {
|
|
190
|
+
left: anchorRect.left,
|
|
191
|
+
right: anchorRect.right,
|
|
192
|
+
top: anchorRect.bottom,
|
|
193
|
+
bottom: anchorRect.bottom + 4,
|
|
194
|
+
width: anchorRect.width,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this._reviewPopup.setItems([{ label: reviewLabel }])
|
|
198
|
+
this._reviewPopup.showAt(belowRect)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
hideReviewPopup() {
|
|
202
|
+
if (this._reviewPopup) {
|
|
203
|
+
this._reviewPopup.hide()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
findFormController() {
|
|
208
|
+
const popup = this.element.closest('#comments-popup')
|
|
209
|
+
if (!popup) return null
|
|
210
|
+
return this.application.getControllerForElementAndIdentifier(popup, 'comments--form')
|
|
211
|
+
}
|
|
212
|
+
|
|
111
213
|
updateReactionsUI(reactionsData) {
|
|
112
214
|
let reactionsContainer = this.element.querySelector('.comment-reactions')
|
|
113
215
|
|
|
@@ -14,6 +14,10 @@ export default class extends Controller {
|
|
|
14
14
|
'imageInput',
|
|
15
15
|
'imageButton',
|
|
16
16
|
'attachmentList',
|
|
17
|
+
'quotedCommentId',
|
|
18
|
+
'quotedText',
|
|
19
|
+
'quoteIndicator',
|
|
20
|
+
'quoteIndicatorText',
|
|
17
21
|
]
|
|
18
22
|
|
|
19
23
|
connect() {
|
|
@@ -154,6 +158,7 @@ export default class extends Controller {
|
|
|
154
158
|
if (this.cancelTarget) this.cancelTarget.style.display = 'none'
|
|
155
159
|
this.presenceController?.clearManualTypingMessage()
|
|
156
160
|
this.clearImageAttachments()
|
|
161
|
+
this.cancelQuote()
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
setSendingState(isSending) {
|
|
@@ -499,6 +504,24 @@ export default class extends Controller {
|
|
|
499
504
|
})
|
|
500
505
|
}
|
|
501
506
|
|
|
507
|
+
quoteComment(commentId, selectedText) {
|
|
508
|
+
if (!commentId || !selectedText) return
|
|
509
|
+
this.quotedCommentIdTarget.value = commentId
|
|
510
|
+
this.quotedTextTarget.value = selectedText
|
|
511
|
+
this.quoteIndicatorTarget.style.display = ''
|
|
512
|
+
this.quoteIndicatorTextTarget.textContent = selectedText.length > 80
|
|
513
|
+
? selectedText.substring(0, 80) + '…'
|
|
514
|
+
: selectedText
|
|
515
|
+
this.focusTextarea()
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
cancelQuote() {
|
|
519
|
+
this.quotedCommentIdTarget.value = ''
|
|
520
|
+
this.quotedTextTarget.value = ''
|
|
521
|
+
this.quoteIndicatorTarget.style.display = 'none'
|
|
522
|
+
this.quoteIndicatorTextTarget.textContent = ''
|
|
523
|
+
}
|
|
524
|
+
|
|
502
525
|
renderCommentHtml(html, { replaceExisting = false } = {}) {
|
|
503
526
|
const listElement = document.getElementById('comments-list')
|
|
504
527
|
if (!listElement || !html) return
|
|
@@ -14,6 +14,7 @@ module Collavre
|
|
|
14
14
|
belongs_to :approver, class_name: Collavre.configuration.user_class_name, optional: true
|
|
15
15
|
belongs_to :action_executed_by, class_name: Collavre.configuration.user_class_name, optional: true
|
|
16
16
|
belongs_to :topic, class_name: "Collavre::Topic", optional: true
|
|
17
|
+
belongs_to :quoted_comment, class_name: "Collavre::Comment", optional: true
|
|
17
18
|
has_many :activity_logs, class_name: "Collavre::ActivityLog", dependent: :destroy
|
|
18
19
|
has_many :comment_reactions, class_name: "Collavre::CommentReaction", dependent: :destroy
|
|
19
20
|
|
|
@@ -32,6 +33,10 @@ module Collavre
|
|
|
32
33
|
after_update_commit :broadcast_update
|
|
33
34
|
after_destroy_commit :broadcast_destroy, :broadcast_badges, :cancel_pending_tasks
|
|
34
35
|
|
|
36
|
+
def review_message?
|
|
37
|
+
quoted_comment_id.present?
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
# public for db migration
|
|
36
41
|
def creative_snippet
|
|
37
42
|
creative.creative_snippet
|
|
@@ -7,5 +7,20 @@ module Collavre
|
|
|
7
7
|
|
|
8
8
|
validates :emoji, presence: true, length: { maximum: 16 }
|
|
9
9
|
validates :user_id, uniqueness: { scope: [ :comment_id, :emoji ] }
|
|
10
|
+
|
|
11
|
+
def self.broadcast_reaction_update(comment)
|
|
12
|
+
creative = comment.creative
|
|
13
|
+
reactions = comment.comment_reactions.reload.to_a
|
|
14
|
+
payload = reactions.group_by(&:emoji).map do |emoji, grouped|
|
|
15
|
+
{ emoji: emoji, count: grouped.size, user_ids: grouped.map(&:user_id) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Turbo::StreamsChannel.broadcast_action_to(
|
|
19
|
+
[ creative, :comments ],
|
|
20
|
+
action: "update_reactions",
|
|
21
|
+
target: "comment_#{comment.id}",
|
|
22
|
+
attributes: { data: payload.to_json }
|
|
23
|
+
)
|
|
24
|
+
end
|
|
10
25
|
end
|
|
11
26
|
end
|
|
@@ -16,6 +16,10 @@ module Collavre
|
|
|
16
16
|
# Log start action
|
|
17
17
|
log_action("start", { message: "Starting agent execution" })
|
|
18
18
|
|
|
19
|
+
# Set original comment early so build_messages can check review_eligible?
|
|
20
|
+
target_comment_id = @context.dig("comment", "id")
|
|
21
|
+
@original_comment = target_comment_id ? Comment.find_by(id: target_comment_id) : nil
|
|
22
|
+
|
|
19
23
|
# Prepare messages for AI
|
|
20
24
|
messages = build_messages
|
|
21
25
|
|
|
@@ -46,8 +50,6 @@ module Collavre
|
|
|
46
50
|
rendered_system_prompt = "#{rendered_system_prompt}\n\n#{collaboration_prompt}" if collaboration_prompt.present?
|
|
47
51
|
|
|
48
52
|
# Create a placeholder comment to stream into
|
|
49
|
-
target_comment_id = @context.dig("comment", "id")
|
|
50
|
-
@original_comment = target_comment_id ? Comment.find_by(id: target_comment_id) : nil
|
|
51
53
|
@reply_comment = nil
|
|
52
54
|
|
|
53
55
|
if @original_comment
|
|
@@ -101,13 +103,22 @@ module Collavre
|
|
|
101
103
|
# Final save to ensure everything is consistent and trigger final callbacks
|
|
102
104
|
if @reply_comment
|
|
103
105
|
if @response_content.present?
|
|
104
|
-
#
|
|
105
|
-
@
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
# Review message handling: update the quoted comment if agent has edit permission
|
|
107
|
+
if @original_comment&.review_message? && handle_review_message(@response_content)
|
|
108
|
+
# React to the review message with a completion emoji
|
|
109
|
+
add_review_completion_reaction(@original_comment)
|
|
110
|
+
# Preserve activity logs by moving them to the quoted comment before destroying placeholder
|
|
111
|
+
reassociate_activity_logs(@reply_comment, @original_comment.quoted_comment)
|
|
112
|
+
@reply_comment.destroy!
|
|
113
|
+
else
|
|
114
|
+
# Force dirty tracking since update_column bypassed it during streaming
|
|
115
|
+
@reply_comment.content_will_change!
|
|
116
|
+
@reply_comment.update!(content: @response_content)
|
|
117
|
+
log_action("reply_created", { comment_id: @reply_comment.id, content: @response_content })
|
|
118
|
+
|
|
119
|
+
# Re-associate activity logs from the trigger comment to the reply comment
|
|
120
|
+
reassociate_activity_logs(@original_comment, @reply_comment)
|
|
121
|
+
end
|
|
111
122
|
else
|
|
112
123
|
@reply_comment.destroy!
|
|
113
124
|
end
|
|
@@ -223,11 +234,68 @@ module Collavre
|
|
|
223
234
|
end
|
|
224
235
|
|
|
225
236
|
payload_text = @context.dig("comment", "content") || @context.to_json
|
|
237
|
+
|
|
238
|
+
# Add review context only when the review will actually result in an in-place update.
|
|
239
|
+
# This prevents misleading the AI when quoting non-agent or ineligible comments.
|
|
240
|
+
if review_eligible?
|
|
241
|
+
quoted_body = @original_comment.quoted_comment&.content
|
|
242
|
+
review_context = I18n.t("collavre.ai_agent.review.context")
|
|
243
|
+
review_parts = [ review_context ]
|
|
244
|
+
review_parts << "---\nOriginal message:\n#{quoted_body}\n---" if quoted_body.present?
|
|
245
|
+
payload_text = "#{review_parts.join("\n\n")}\n\n#{payload_text}"
|
|
246
|
+
end
|
|
247
|
+
|
|
226
248
|
messages << { role: "user", parts: [ { text: payload_text } ] }
|
|
227
249
|
|
|
228
250
|
messages
|
|
229
251
|
end
|
|
230
252
|
|
|
253
|
+
def review_eligible?
|
|
254
|
+
return false unless @original_comment&.review_message?
|
|
255
|
+
|
|
256
|
+
quoted_comment = @original_comment.quoted_comment
|
|
257
|
+
return false unless quoted_comment
|
|
258
|
+
return false unless quoted_comment.user_id == @agent.id
|
|
259
|
+
return false unless quoted_comment.creative_id == @original_comment.creative_id
|
|
260
|
+
# Ensure the quoted comment is not private (prevent privilege bypass via client-supplied IDs)
|
|
261
|
+
return false if quoted_comment.private?
|
|
262
|
+
# Ensure both comments are in the same topic to prevent cross-topic overwrites
|
|
263
|
+
return false unless quoted_comment.topic_id == @original_comment.topic_id
|
|
264
|
+
|
|
265
|
+
true
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def handle_review_message(response_content)
|
|
269
|
+
return false unless review_eligible?
|
|
270
|
+
|
|
271
|
+
quoted_comment = @original_comment.quoted_comment
|
|
272
|
+
quoted_comment.update!(content: response_content)
|
|
273
|
+
log_action("review_updated", {
|
|
274
|
+
quoted_comment_id: quoted_comment.id,
|
|
275
|
+
original_comment_id: @original_comment.id,
|
|
276
|
+
content: response_content
|
|
277
|
+
})
|
|
278
|
+
true
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def add_review_completion_reaction(comment)
|
|
282
|
+
reaction = begin
|
|
283
|
+
CommentReaction.find_or_create_by!(
|
|
284
|
+
comment: comment,
|
|
285
|
+
user: @agent,
|
|
286
|
+
emoji: "✅"
|
|
287
|
+
)
|
|
288
|
+
rescue ActiveRecord::RecordNotUnique
|
|
289
|
+
# Already reacted via race condition, fetch existing
|
|
290
|
+
CommentReaction.find_by(comment: comment, user: @agent, emoji: "✅")
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
CommentReaction.broadcast_reaction_update(comment) if reaction
|
|
294
|
+
rescue StandardError => e
|
|
295
|
+
# Reaction is supplementary; log but don't fail the review update
|
|
296
|
+
Rails.logger.warn("[AiAgentService] Failed to add review reaction: #{e.message}")
|
|
297
|
+
end
|
|
298
|
+
|
|
231
299
|
def reply_to_comment(comment_id, content)
|
|
232
300
|
original_comment = Comment.find_by(id: comment_id)
|
|
233
301
|
return unless original_comment
|
|
@@ -43,7 +43,7 @@ module Collavre
|
|
|
43
43
|
"token_spike_window_minutes" => 10
|
|
44
44
|
},
|
|
45
45
|
"stuck_detection" => {
|
|
46
|
-
"enabled" =>
|
|
46
|
+
"enabled" => false,
|
|
47
47
|
"task_stuck_threshold_minutes" => 30, # Task running for > N minutes
|
|
48
48
|
"creative_stall_threshold_minutes" => 120, # Creative no progress for > N minutes
|
|
49
49
|
"create_system_comment" => true # Create system comment on escalation
|
|
@@ -267,7 +267,7 @@ module Collavre
|
|
|
267
267
|
[
|
|
268
268
|
"collavre.stuck_detection.creative_stalled",
|
|
269
269
|
{
|
|
270
|
-
creative_title: creative.
|
|
270
|
+
creative_title: creative.creative_snippet,
|
|
271
271
|
hours: hours_stuck,
|
|
272
272
|
progress: ((creative.progress || 0) * 100).round
|
|
273
273
|
}
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
<%= t('collavre.comments.delete_button') %>
|
|
67
67
|
</button>
|
|
68
68
|
</div>
|
|
69
|
+
<% if comment.quoted_text.present? %>
|
|
70
|
+
<div class="comment-quoted-block">
|
|
71
|
+
<blockquote class="comment-quoted-text"><%= comment.quoted_text %></blockquote>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
69
74
|
<div class="comment-content"><%= comment.content %></div>
|
|
70
75
|
|
|
71
76
|
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
data-move-error-text="<%= t('collavre.comments.move_error') %>"
|
|
32
32
|
data-hint-drag-topic-text="<%= t('collavre.comments.hint_drag_topic') %>"
|
|
33
33
|
data-hint-move-button-text="<%= t('collavre.comments.hint_move_button') %>"
|
|
34
|
-
data-add-participant-text="<%= t('collavre.comments.add_participant') %>"
|
|
34
|
+
data-add-participant-text="<%= t('collavre.comments.add_participant') %>"
|
|
35
|
+
data-review-button-text="<%= t('collavre.comments.review_button') %>">
|
|
35
36
|
<div class="resize-handle resize-handle-left" data-comments--popup-target="leftHandle"></div>
|
|
36
37
|
<div class="resize-handle resize-handle-right" data-comments--popup-target="rightHandle"></div>
|
|
37
38
|
<div class="comments-popup-header">
|
|
@@ -62,6 +63,12 @@
|
|
|
62
63
|
<div id="comments-list" data-comments--popup-target="list" data-comments--list-target="list"><%= t('app.loading') %></div>
|
|
63
64
|
<div id="typing-indicator" data-comments--presence-target="typingIndicator"></div>
|
|
64
65
|
<form id="new-comment-form" data-comments--popup-target="form" data-comments--form-target="form" style="display:none;">
|
|
66
|
+
<input type="hidden" name="comment[quoted_comment_id]" data-comments--form-target="quotedCommentId" value="" />
|
|
67
|
+
<input type="hidden" name="comment[quoted_text]" data-comments--form-target="quotedText" value="" />
|
|
68
|
+
<div class="comment-quote-indicator" data-comments--form-target="quoteIndicator" style="display:none;">
|
|
69
|
+
<span class="comment-quote-indicator-text" data-comments--form-target="quoteIndicatorText"></span>
|
|
70
|
+
<button type="button" class="comment-quote-cancel" data-action="click->comments--form#cancelQuote" title="<%= t('app.cancel') %>">×</button>
|
|
71
|
+
</div>
|
|
65
72
|
<textarea class="shared-input-surface" name="comment[content]" data-comments--form-target="textarea" data-comments--presence-target="textarea" data-comments--mention-menu-target="textarea" rows="2" enterkeyhint="send"></textarea>
|
|
66
73
|
<div class="comment-bottom">
|
|
67
74
|
<input type="file" id="comment-images" name="comment[images][]" accept="image/*" multiple data-comments--form-target="imageInput" style="display:none;" />
|
|
@@ -202,8 +202,6 @@
|
|
|
202
202
|
|
|
203
203
|
<%= render 'set_plan_modal' %>
|
|
204
204
|
<% if can_manage_integrations %>
|
|
205
|
-
<%# Legacy hardcoded integration modals %>
|
|
206
|
-
<%= render 'collavre_github/integrations/modal', creative: current_creative %>
|
|
207
205
|
<%# Dynamically registered integration modals %>
|
|
208
206
|
<%= render 'integration_modals', creative: current_creative %>
|
|
209
207
|
<% end %>
|
|
@@ -23,6 +23,8 @@ en:
|
|
|
23
23
|
focus_instruction: "- Focus on answering the request"
|
|
24
24
|
completion_instruction: "- Clearly communicate results when done"
|
|
25
25
|
followup_instruction: "- If you need more information, ask the requester via @mention"
|
|
26
|
+
review:
|
|
27
|
+
context: "The user is reviewing your previous message and requesting changes. Your response will replace the original message. Provide a complete rewrite."
|
|
26
28
|
collaboration:
|
|
27
29
|
header: "## Available Agents for Collaboration"
|
|
28
30
|
escalation_header: "### Escalation Targets (when stuck)"
|
|
@@ -23,6 +23,8 @@ ko:
|
|
|
23
23
|
focus_instruction: "- 요청에 집중해서 답변하세요"
|
|
24
24
|
completion_instruction: "- 완료되면 결과를 명확히 전달하세요"
|
|
25
25
|
followup_instruction: "- 추가 정보가 필요하면 요청자에게 @멘션으로 물어보세요"
|
|
26
|
+
review:
|
|
27
|
+
context: "사용자가 당신의 이전 메시지를 리뷰하고 변경을 요청하고 있습니다. 당신의 응답이 원본 메시지를 대체합니다. 전체 내용을 다시 작성해주세요."
|
|
26
28
|
collaboration:
|
|
27
29
|
header: "## 협업 가능한 Agent"
|
|
28
30
|
escalation_header: "### 에스컬레이션 대상 (문제 해결 불가 시)"
|
|
@@ -65,6 +65,7 @@ en:
|
|
|
65
65
|
voice_stop: Stop
|
|
66
66
|
speech_unavailable: Speech recognition is not supported in this browser.
|
|
67
67
|
move_button: Move
|
|
68
|
+
review_button: Review
|
|
68
69
|
move_no_selection: Select at least one message to move.
|
|
69
70
|
move_error: Unable to move messages.
|
|
70
71
|
add_participant: Add user
|
data/lib/collavre/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: collavre
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Collavre
|
|
@@ -577,6 +577,7 @@ files:
|
|
|
577
577
|
- db/migrate/20260206005035_create_orchestrator_policies.rb
|
|
578
578
|
- db/migrate/20260206094509_add_retry_count_to_tasks.rb
|
|
579
579
|
- db/migrate/20260206100000_add_topic_id_to_tasks.rb
|
|
580
|
+
- db/migrate/20260212011655_add_quoted_comment_to_comments.rb
|
|
580
581
|
- lib/collavre.rb
|
|
581
582
|
- lib/collavre/configuration.rb
|
|
582
583
|
- lib/collavre/engine.rb
|