collavre 0.13.0 → 0.14.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 +16 -2
- data/app/assets/stylesheets/collavre/design_tokens.css +1 -0
- data/app/assets/stylesheets/collavre/popup.css +6 -0
- data/app/components/collavre/inbox/badge_component.rb +1 -2
- data/app/controllers/collavre/comment_read_pointers_controller.rb +1 -1
- data/app/controllers/collavre/comments/reactions_controller.rb +2 -16
- data/app/controllers/collavre/comments/versions_controller.rb +2 -15
- data/app/controllers/collavre/comments_controller.rb +5 -17
- data/app/controllers/collavre/creatives_controller.rb +3 -3
- data/app/controllers/collavre/tasks_controller.rb +1 -25
- data/app/controllers/collavre/topics_controller.rb +7 -3
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +14 -8
- data/app/controllers/concerns/collavre/comments/comment_scoping.rb +20 -0
- data/app/controllers/concerns/collavre/integration_permission.rb +31 -0
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +1 -1
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +2 -0
- data/app/helpers/collavre/application_helper.rb +2 -3
- data/app/helpers/collavre/creatives_helper.rb +1 -9
- data/app/helpers/collavre/navigation_helper.rb +0 -6
- data/app/javascript/controllers/agent_trigger_controller.js +94 -0
- data/app/javascript/controllers/comment_controller.js +2 -2
- data/app/javascript/controllers/comments/form_controller.js +80 -0
- data/app/javascript/controllers/comments/list_controller.js +58 -22
- data/app/javascript/controllers/comments/popup_controller.js +7 -0
- data/app/javascript/controllers/comments/topics_controller.js +16 -4
- data/app/javascript/controllers/index.js +3 -0
- data/app/models/collavre/comment/broadcastable.rb +1 -1
- data/app/models/collavre/comment.rb +8 -1
- data/app/models/collavre/creative/permissible.rb +23 -0
- data/app/models/collavre/creative.rb +0 -6
- data/app/models/collavre/inbox_item.rb +0 -2
- data/app/models/collavre/topic.rb +2 -0
- data/app/services/collavre/ai_agent_service.rb +5 -16
- data/app/services/collavre/comment_move_service.rb +1 -4
- data/app/services/collavre/inbox_reply_service.rb +87 -0
- data/app/services/collavre/openclaw_abort_service.rb +45 -0
- data/app/services/collavre/orchestration/agent_context_builder.rb +5 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +7 -11
- data/app/services/collavre/topic_branch_service.rb +112 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +4 -2
- data/app/views/collavre/shared/_custom_theme_style.html.erb +6 -2
- data/app/views/collavre/shared/navigation/_search_form.html.erb +0 -17
- data/app/views/collavre/users/_trigger_field.html.erb +51 -0
- data/app/views/collavre/users/edit_ai.html.erb +1 -5
- data/app/views/collavre/users/new_ai.html.erb +1 -5
- data/config/locales/comments.en.yml +8 -0
- data/config/locales/comments.ko.yml +8 -0
- data/config/locales/users.en.yml +10 -0
- data/config/locales/users.ko.yml +10 -0
- data/config/routes.rb +1 -1
- data/db/migrate/20260409000000_add_source_topic_id_to_topics.rb +5 -0
- data/lib/collavre/version.rb +1 -1
- metadata +9 -4
- data/app/helpers/collavre/user_themes_helper.rb +0 -4
- data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +0 -36
- data/app/views/collavre/creatives/_share_button.html.erb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e56759c8d6833552a9d5e49b19745aca7d0a58d56ad3c3a7e6d502ea54d6b156
|
|
4
|
+
data.tar.gz: 37a2481e6fc5abc1077167d481abb51b118f256de05c704cdd64381eb090562a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9261164e029e28e9303f30879740f5dbd3ccbb6779b0767cd8488920e56f0037f63159fcdf64bd12dbb7788a22f5e1718fc468f616a02f59553a67713dfa422c
|
|
7
|
+
data.tar.gz: 5856204cfe3ff27016d1e0f5a54f3de43cf839911fae1dba656200679c941c837b4f30ded9e9f94153ecea2972c8797a0554f81c58df27123f1dc0d66ed7b30a
|
|
@@ -43,7 +43,7 @@ body.chat-fullscreen {
|
|
|
43
43
|
border-radius: 0;
|
|
44
44
|
box-shadow: none;
|
|
45
45
|
border: none;
|
|
46
|
-
z-index:
|
|
46
|
+
z-index: var(--layer-fullscreen);
|
|
47
47
|
box-sizing: border-box;
|
|
48
48
|
padding: 0.5em;
|
|
49
49
|
}
|
|
@@ -478,6 +478,11 @@ body.chat-fullscreen {
|
|
|
478
478
|
white-space: nowrap;
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
.inbox-reply-btn {
|
|
482
|
+
font-size: var(--text-0) !important;
|
|
483
|
+
white-space: nowrap;
|
|
484
|
+
}
|
|
485
|
+
|
|
481
486
|
.comment-highlight {
|
|
482
487
|
background: color-mix(in srgb, var(--color-active) 15%, transparent) !important;
|
|
483
488
|
transition: background 0.3s ease;
|
|
@@ -599,7 +604,7 @@ body.chat-fullscreen {
|
|
|
599
604
|
border: 1px solid var(--color-border);
|
|
600
605
|
border-radius: 8px;
|
|
601
606
|
box-shadow: var(--shadow-3);
|
|
602
|
-
z-index:
|
|
607
|
+
z-index: var(--layer-fullscreen);
|
|
603
608
|
}
|
|
604
609
|
|
|
605
610
|
#global-reaction-picker {
|
|
@@ -1510,6 +1515,15 @@ body.chat-fullscreen {
|
|
|
1510
1515
|
opacity: 0.5;
|
|
1511
1516
|
font-style: italic;
|
|
1512
1517
|
}
|
|
1518
|
+
.topic-branch-icon {
|
|
1519
|
+
font-size: 0.75em;
|
|
1520
|
+
opacity: 0.6;
|
|
1521
|
+
margin-right: 1px;
|
|
1522
|
+
cursor: pointer;
|
|
1523
|
+
}
|
|
1524
|
+
.topic-branch-icon:hover {
|
|
1525
|
+
opacity: 1;
|
|
1526
|
+
}
|
|
1513
1527
|
.archive-topic-btn,
|
|
1514
1528
|
.unarchive-topic-btn {
|
|
1515
1529
|
background: none;
|
|
@@ -86,6 +86,12 @@
|
|
|
86
86
|
z-index: calc(var(--layer-modal) + 10);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* When chat is fullscreen, these modals must sit above it */
|
|
90
|
+
body.chat-fullscreen #link-creative-modal,
|
|
91
|
+
body.chat-fullscreen #topic-search-modal {
|
|
92
|
+
z-index: calc(var(--layer-fullscreen) + 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
.common-popup {
|
|
90
96
|
position: absolute;
|
|
91
97
|
z-index: var(--layer-modal);
|
|
@@ -26,8 +26,7 @@ module Collavre
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
28
|
def unread_count_for_creative
|
|
29
|
-
visible_comments = @creative.comments.
|
|
30
|
-
.or(@creative.comments.where(user_id: @user.id))
|
|
29
|
+
visible_comments = @creative.comments.visible_to(@user)
|
|
31
30
|
pointer = CommentReadPointer.find_by(user: @user, creative: @creative)
|
|
32
31
|
last_read_id = pointer&.last_read_comment_id
|
|
33
32
|
|
|
@@ -2,7 +2,7 @@ module Collavre
|
|
|
2
2
|
class CommentReadPointersController < ApplicationController
|
|
3
3
|
def update
|
|
4
4
|
creative = Creative.find(params[:creative_id]).effective_origin
|
|
5
|
-
last_id = creative.comments.
|
|
5
|
+
last_id = creative.comments.visible_to(Current.user).maximum(:id)
|
|
6
6
|
pointer = CommentReadPointer.find_or_initialize_by(user: Current.user, creative: creative)
|
|
7
7
|
|
|
8
8
|
previous_last_read_id = pointer.last_read_comment_id
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
module Comments
|
|
3
3
|
class ReactionsController < ApplicationController
|
|
4
|
+
include Collavre::Comments::CommentScoping
|
|
5
|
+
|
|
4
6
|
before_action :set_creative
|
|
5
7
|
before_action :set_comment
|
|
6
8
|
before_action :authorize_feedback!
|
|
@@ -48,22 +50,6 @@ module Collavre
|
|
|
48
50
|
CommentReaction.broadcast_reaction_update(@comment)
|
|
49
51
|
end
|
|
50
52
|
|
|
51
|
-
def set_creative
|
|
52
|
-
@creative = Creative.find(params[:creative_id]).effective_origin
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def set_comment
|
|
56
|
-
comment_id = params[:comment_id] || params[:id]
|
|
57
|
-
@comment = @creative.comments
|
|
58
|
-
.where(
|
|
59
|
-
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
60
|
-
false,
|
|
61
|
-
Current.user.id,
|
|
62
|
-
Current.user.id
|
|
63
|
-
)
|
|
64
|
-
.find(comment_id)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
53
|
def authorize_feedback!
|
|
68
54
|
return if @creative.has_permission?(Current.user, :feedback)
|
|
69
55
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Collavre
|
|
4
4
|
module Comments
|
|
5
5
|
class VersionsController < ApplicationController
|
|
6
|
+
include Collavre::Comments::CommentScoping
|
|
7
|
+
|
|
6
8
|
before_action :set_creative
|
|
7
9
|
before_action :set_comment
|
|
8
10
|
|
|
@@ -62,21 +64,6 @@ module Collavre
|
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
private
|
|
65
|
-
|
|
66
|
-
def set_creative
|
|
67
|
-
@creative = Creative.find(params[:creative_id]).effective_origin
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def set_comment
|
|
71
|
-
@comment = @creative.comments
|
|
72
|
-
.where(
|
|
73
|
-
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
74
|
-
false,
|
|
75
|
-
Current.user.id,
|
|
76
|
-
Current.user.id
|
|
77
|
-
)
|
|
78
|
-
.find(params[:comment_id])
|
|
79
|
-
end
|
|
80
67
|
end
|
|
81
68
|
end
|
|
82
69
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CommentsController < ApplicationController
|
|
3
|
+
include Collavre::Comments::CommentScoping
|
|
3
4
|
include Collavre::Comments::ApprovalActions
|
|
4
5
|
include Collavre::Comments::Conversion
|
|
5
6
|
include Collavre::Comments::BatchOperations
|
|
@@ -26,12 +27,7 @@ module Collavre
|
|
|
26
27
|
def index
|
|
27
28
|
limit = 20
|
|
28
29
|
|
|
29
|
-
visible_scope = @creative.comments.
|
|
30
|
-
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
31
|
-
false,
|
|
32
|
-
Current.user.id,
|
|
33
|
-
Current.user.id
|
|
34
|
-
)
|
|
30
|
+
visible_scope = @creative.comments.visible_to(Current.user)
|
|
35
31
|
scope = visible_scope.with_attached_images.includes(:topic, :comment_reactions, :comment_versions, :snapshot_as_result)
|
|
36
32
|
|
|
37
33
|
if params[:search].present?
|
|
@@ -185,6 +181,9 @@ module Collavre
|
|
|
185
181
|
@comment.skip_dispatch = true
|
|
186
182
|
end
|
|
187
183
|
if @comment.save
|
|
184
|
+
# Cross-post inbox inline replies to the original creative/topic
|
|
185
|
+
InboxReplyService.call(@comment)
|
|
186
|
+
|
|
188
187
|
# Dispatch is handled by Comment#after_create_commit callback
|
|
189
188
|
@comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
|
|
190
189
|
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }, status: :created
|
|
@@ -353,17 +352,6 @@ module Collavre
|
|
|
353
352
|
end
|
|
354
353
|
end
|
|
355
354
|
|
|
356
|
-
def set_comment
|
|
357
|
-
@comment = @creative.comments
|
|
358
|
-
.where(
|
|
359
|
-
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
360
|
-
false,
|
|
361
|
-
Current.user.id,
|
|
362
|
-
Current.user.id
|
|
363
|
-
)
|
|
364
|
-
.find(params[:id])
|
|
365
|
-
end
|
|
366
|
-
|
|
367
355
|
def comment_params
|
|
368
356
|
params.require(:comment).permit(:content, :private, :topic_id, :quoted_comment_id, :quoted_text, :review_type, images: [])
|
|
369
357
|
end
|
|
@@ -528,7 +528,7 @@ module Collavre
|
|
|
528
528
|
end
|
|
529
529
|
|
|
530
530
|
topic = creative.topics.find_by(name: "Drop Trigger")
|
|
531
|
-
agent = parent.
|
|
531
|
+
agent = parent.find_ai_agent(:write)
|
|
532
532
|
unless topic && agent
|
|
533
533
|
Rails.logger.warn("[TriggerAction] resume: missing topic=#{topic&.id} or agent for creative #{creative.id}")
|
|
534
534
|
return
|
|
@@ -565,7 +565,7 @@ module Collavre
|
|
|
565
565
|
end
|
|
566
566
|
|
|
567
567
|
topic = creative.topics.find_by(name: "Drop Trigger")
|
|
568
|
-
agent = parent.
|
|
568
|
+
agent = parent.find_ai_agent(:write)
|
|
569
569
|
unless topic && agent
|
|
570
570
|
Rails.logger.warn("[TriggerAction] restart: missing topic=#{topic&.id} or agent for creative #{creative.id}")
|
|
571
571
|
return
|
|
@@ -593,7 +593,7 @@ module Collavre
|
|
|
593
593
|
end
|
|
594
594
|
|
|
595
595
|
def notify_drop_trigger_missing_agent!(creative)
|
|
596
|
-
return if creative.
|
|
596
|
+
return if creative.find_ai_agent(:write)
|
|
597
597
|
|
|
598
598
|
topic = creative.topics.find_or_create_by!(name: "Drop Trigger") do |t|
|
|
599
599
|
t.user = creative.user
|
|
@@ -24,31 +24,7 @@ module Collavre
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
def abort_openclaw_session(task)
|
|
27
|
-
|
|
28
|
-
return unless defined?(CollavreOpenclaw::ConnectionManager)
|
|
29
|
-
|
|
30
|
-
conn = CollavreOpenclaw::ConnectionManager.instance.connection_for(task.agent)
|
|
31
|
-
session_key = build_session_key(task)
|
|
32
|
-
conn.chat_abort(session_key: session_key)
|
|
33
|
-
rescue StandardError => e
|
|
34
|
-
Rails.logger.warn("[TasksController] OpenClaw abort failed: #{e.message}")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def build_session_key(task)
|
|
38
|
-
payload = task.trigger_event_payload || {}
|
|
39
|
-
creative = task.creative || Creative.find_by(id: payload.dig("creative", "id"))
|
|
40
|
-
comment = Comment.find_by(id: payload.dig("comment", "id"))
|
|
41
|
-
|
|
42
|
-
CollavreOpenclaw::OpenclawAdapter.new(
|
|
43
|
-
user: task.agent,
|
|
44
|
-
system_prompt: "",
|
|
45
|
-
context: {
|
|
46
|
-
creative: creative,
|
|
47
|
-
user: task.agent,
|
|
48
|
-
task: task,
|
|
49
|
-
comment: comment
|
|
50
|
-
}
|
|
51
|
-
).session_key
|
|
27
|
+
Collavre::OpenclawAbortService.call(agent: task.agent, task: task)
|
|
52
28
|
end
|
|
53
29
|
end
|
|
54
30
|
end
|
|
@@ -17,12 +17,16 @@ module Collavre
|
|
|
17
17
|
.pick(:last_topic_id)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
system_topic_id = @creative.inbox? ? @creative.topics.find_by(name: Creative::SYSTEM_TOPIC_NAME)&.id : nil
|
|
21
|
+
|
|
20
22
|
render json: {
|
|
21
23
|
topics: active_topics.map { |t| topic_json(t) },
|
|
22
24
|
archived_topics: archived_topics,
|
|
23
25
|
can_manage: can_manage,
|
|
24
26
|
can_create_topic: can_create_topic,
|
|
25
|
-
last_topic_id: last_topic_id
|
|
27
|
+
last_topic_id: last_topic_id,
|
|
28
|
+
is_inbox: @creative.inbox?,
|
|
29
|
+
system_topic_id: system_topic_id
|
|
26
30
|
}
|
|
27
31
|
end
|
|
28
32
|
|
|
@@ -268,7 +272,7 @@ module Collavre
|
|
|
268
272
|
end
|
|
269
273
|
|
|
270
274
|
def topic_json(topic)
|
|
271
|
-
data = topic.slice(:id, :name)
|
|
275
|
+
data = topic.slice(:id, :name, :source_topic_id)
|
|
272
276
|
agent = topic.instance_variable_get(:@_primary_agent) || topic.primary_agent
|
|
273
277
|
if agent
|
|
274
278
|
data[:primary_agent] = agent_json(agent)
|
|
@@ -277,7 +281,7 @@ module Collavre
|
|
|
277
281
|
end
|
|
278
282
|
|
|
279
283
|
def topic_json_with_agent(topic, agent)
|
|
280
|
-
data = topic.slice(:id, :name)
|
|
284
|
+
data = topic.slice(:id, :name, :source_topic_id)
|
|
281
285
|
data[:primary_agent] = agent_json(agent)
|
|
282
286
|
data
|
|
283
287
|
end
|
|
@@ -14,10 +14,7 @@ module Collavre
|
|
|
14
14
|
is_admin = @creative.has_permission?(Current.user, :admin)
|
|
15
15
|
is_creative_owner = @creative.user == Current.user
|
|
16
16
|
|
|
17
|
-
visible_scope = @creative.comments.
|
|
18
|
-
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
19
|
-
false, Current.user.id, Current.user.id
|
|
20
|
-
)
|
|
17
|
+
visible_scope = @creative.comments.visible_to(Current.user)
|
|
21
18
|
comments = visible_scope.where(id: comment_ids).to_a
|
|
22
19
|
|
|
23
20
|
if comments.length != comment_ids.length
|
|
@@ -47,10 +44,7 @@ module Collavre
|
|
|
47
44
|
render json: { error: I18n.t("collavre.comments.merge.not_authorized") }, status: :forbidden and return
|
|
48
45
|
end
|
|
49
46
|
|
|
50
|
-
visible_scope = @creative.comments.
|
|
51
|
-
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
52
|
-
false, Current.user.id, Current.user.id
|
|
53
|
-
)
|
|
47
|
+
visible_scope = @creative.comments.visible_to(Current.user)
|
|
54
48
|
comments = visible_scope.where(id: comment_ids).to_a
|
|
55
49
|
|
|
56
50
|
if comments.length != comment_ids.length
|
|
@@ -82,6 +76,18 @@ module Collavre
|
|
|
82
76
|
rescue ActiveRecord::RecordInvalid => e
|
|
83
77
|
render json: { error: e.record.errors.full_messages.to_sentence.presence || I18n.t("collavre.comments.move_error") }, status: :unprocessable_entity
|
|
84
78
|
end
|
|
79
|
+
|
|
80
|
+
def branch
|
|
81
|
+
source_topic = params[:topic_id].present? ? @creative.topics.find(params[:topic_id]) : nil
|
|
82
|
+
new_topic = TopicBranchService.new(creative: @creative, user: Current.user, source_topic: source_topic).call(
|
|
83
|
+
comment_ids: params[:comment_ids]
|
|
84
|
+
)
|
|
85
|
+
render json: { success: true, topic: new_topic.slice(:id, :name, :source_topic_id) }, status: :created
|
|
86
|
+
rescue TopicBranchService::BranchError => e
|
|
87
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
88
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
89
|
+
render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_entity
|
|
90
|
+
end
|
|
85
91
|
end
|
|
86
92
|
end
|
|
87
93
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Comments
|
|
5
|
+
module CommentScoping
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def set_creative
|
|
11
|
+
@creative = Creative.find(params[:creative_id]).effective_origin
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def set_comment
|
|
15
|
+
comment_id = params[:comment_id] || params[:id]
|
|
16
|
+
@comment = @creative.comments.visible_to(Current.user).find(comment_id)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module IntegrationPermission
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def ensure_read_permission
|
|
10
|
+
return if @creative.has_permission?(Current.user, :read)
|
|
11
|
+
|
|
12
|
+
render json: { error: integration_forbidden_message }, status: :forbidden
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ensure_admin_permission
|
|
16
|
+
return if @creative.has_permission?(Current.user, :admin)
|
|
17
|
+
|
|
18
|
+
render json: { error: integration_forbidden_message }, status: :forbidden
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ensure_write_permission
|
|
22
|
+
return if @creative.has_permission?(Current.user, :write)
|
|
23
|
+
|
|
24
|
+
render json: { error: integration_forbidden_message }, status: :forbidden
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def integration_forbidden_message
|
|
28
|
+
"forbidden"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -5,7 +5,7 @@ module Collavre
|
|
|
5
5
|
included do
|
|
6
6
|
before_action :set_user_for_ai_actions, only: [ :edit_ai, :update_ai ]
|
|
7
7
|
before_action :verify_ai_user, only: [ :edit_ai, :update_ai ]
|
|
8
|
-
before_action :verify_ai_user_authorization, only: [ :update_ai ]
|
|
8
|
+
before_action :verify_ai_user_authorization, only: [ :edit_ai, :update_ai ]
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def new_ai
|
|
@@ -29,6 +29,7 @@ module Collavre
|
|
|
29
29
|
|
|
30
30
|
def update
|
|
31
31
|
@user = Collavre::User.find(params[:id])
|
|
32
|
+
return head :forbidden unless @user == Current.user || Current.user.system_admin?
|
|
32
33
|
if @user.update(profile_params)
|
|
33
34
|
redirect_to user_path(@user), notice: I18n.t("collavre.users.profile_updated")
|
|
34
35
|
else
|
|
@@ -46,6 +47,7 @@ module Collavre
|
|
|
46
47
|
|
|
47
48
|
def edit_password
|
|
48
49
|
@user = Collavre::User.find(params[:id])
|
|
50
|
+
head :forbidden unless @user == Current.user || Current.user.system_admin?
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def passkeys
|
|
@@ -78,8 +78,7 @@ module Collavre
|
|
|
78
78
|
styles << render_theme_media_query(dark_theme, "dark")
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
styles.join("\n").html_safe # rubocop:disable Rails/OutputSafety
|
|
81
|
+
safe_join(styles, "\n")
|
|
83
82
|
end
|
|
84
83
|
|
|
85
84
|
private
|
|
@@ -111,7 +110,7 @@ module Collavre
|
|
|
111
110
|
#
|
|
112
111
|
def render_extension_slot(slot, **locals)
|
|
113
112
|
entries = Collavre::ViewExtensions.for_slot(slot)
|
|
114
|
-
return
|
|
113
|
+
return safe_join([]) if entries.empty?
|
|
115
114
|
|
|
116
115
|
safe_join(entries.map { |entry| render(partial: entry[:partial], locals: locals) })
|
|
117
116
|
end
|
|
@@ -118,7 +118,7 @@ module Collavre
|
|
|
118
118
|
if value == 1 && !completion_mark.nil?
|
|
119
119
|
text = completion_mark
|
|
120
120
|
end
|
|
121
|
-
display_text = text.blank? ? "
|
|
121
|
+
display_text = text.blank? ? "\u00A0\u00A0" : text
|
|
122
122
|
content_tag(
|
|
123
123
|
:span,
|
|
124
124
|
display_text,
|
|
@@ -126,10 +126,6 @@ module Collavre
|
|
|
126
126
|
)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
def expanded_from_expanded_state(creative_id, expanded_state_map)
|
|
130
|
-
!!(expanded_state_map && expanded_state_map[creative_id.to_s])
|
|
131
|
-
end
|
|
132
|
-
|
|
133
129
|
def render_creative_tree_markdown(creatives, level = 1, with_progress = false, max_depth: nil)
|
|
134
130
|
return "" if creatives.blank?
|
|
135
131
|
md = ""
|
|
@@ -184,9 +180,5 @@ module Collavre
|
|
|
184
180
|
def markdown_links_to_html(text, image_refs = {})
|
|
185
181
|
MarkdownConverter.markdown_to_html(text, image_refs)
|
|
186
182
|
end
|
|
187
|
-
|
|
188
|
-
def html_links_to_markdown(text)
|
|
189
|
-
MarkdownConverter.html_to_markdown(text)
|
|
190
|
-
end
|
|
191
183
|
end
|
|
192
184
|
end
|
|
@@ -47,12 +47,6 @@ module Collavre
|
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def render_mobile_navigation_item(item)
|
|
51
|
-
content = render_navigation_item(item, mobile: true)
|
|
52
|
-
return if content.blank?
|
|
53
|
-
content_tag(:div, content)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
50
|
def render_navigation_item_with_children(item, mobile: false)
|
|
57
51
|
return unless navigation_item_visible?(item, desktop: !mobile)
|
|
58
52
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
// Maps trigger type radio buttons to routing_expression values.
|
|
4
|
+
// Shows/hides keyword input and advanced expression textarea based on selection.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ['hiddenField', 'keywordInput', 'keywordGroup', 'advancedGroup', 'advancedInput']
|
|
7
|
+
|
|
8
|
+
static values = {
|
|
9
|
+
currentExpression: String
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
this.detectTriggerType()
|
|
14
|
+
this.updateVisibility()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Called when a radio button changes
|
|
18
|
+
change() {
|
|
19
|
+
this.updateVisibility()
|
|
20
|
+
this.updateExpression()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Called when keyword input changes
|
|
24
|
+
keywordChanged() {
|
|
25
|
+
// Strip double quotes to prevent Liquid injection
|
|
26
|
+
const input = this.keywordInputTarget
|
|
27
|
+
const sanitized = input.value.replace(/"/g, '')
|
|
28
|
+
if (input.value !== sanitized) input.value = sanitized
|
|
29
|
+
this.updateExpression()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Called when advanced expression changes
|
|
33
|
+
advancedChanged() {
|
|
34
|
+
this.hiddenFieldTarget.value = this.advancedInputTarget.value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get selectedType() {
|
|
38
|
+
const checked = this.element.querySelector('input[name="trigger_type"]:checked')
|
|
39
|
+
return checked?.value || 'mention'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
detectTriggerType() {
|
|
43
|
+
const expr = (this.currentExpressionValue || '').trim()
|
|
44
|
+
if (!expr || expr === 'chat.mentioned_user.id == agent.id') {
|
|
45
|
+
this.selectRadio('mention')
|
|
46
|
+
} else if (expr === 'event_name == "comment_created"') {
|
|
47
|
+
this.selectRadio('auto')
|
|
48
|
+
} else {
|
|
49
|
+
const keywordMatch = expr.match(/^chat\.content\s+contains\s+"([^"]*)"$/)
|
|
50
|
+
if (keywordMatch) {
|
|
51
|
+
this.selectRadio('keyword')
|
|
52
|
+
this.keywordInputTarget.value = keywordMatch[1]
|
|
53
|
+
} else {
|
|
54
|
+
this.selectRadio('advanced')
|
|
55
|
+
this.advancedInputTarget.value = expr
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
selectRadio(value) {
|
|
61
|
+
const radio = this.element.querySelector(`input[name="trigger_type"][value="${value}"]`)
|
|
62
|
+
if (radio) radio.checked = true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
updateVisibility() {
|
|
66
|
+
const type = this.selectedType
|
|
67
|
+
this.keywordGroupTarget.style.display = type === 'keyword' ? '' : 'none'
|
|
68
|
+
this.advancedGroupTarget.style.display = type === 'advanced' ? '' : 'none'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
updateExpression() {
|
|
72
|
+
const type = this.selectedType
|
|
73
|
+
let expression = ''
|
|
74
|
+
|
|
75
|
+
switch (type) {
|
|
76
|
+
case 'auto':
|
|
77
|
+
expression = 'event_name == "comment_created"'
|
|
78
|
+
break
|
|
79
|
+
case 'mention':
|
|
80
|
+
expression = 'chat.mentioned_user.id == agent.id'
|
|
81
|
+
break
|
|
82
|
+
case 'keyword': {
|
|
83
|
+
const keyword = this.keywordInputTarget.value.trim().replace(/"/g, '')
|
|
84
|
+
expression = keyword ? `chat.content contains "${keyword}"` : ''
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
case 'advanced':
|
|
88
|
+
expression = this.advancedInputTarget.value
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.hiddenFieldTarget.value = expression
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -121,7 +121,7 @@ export default class extends Controller {
|
|
|
121
121
|
|
|
122
122
|
// Text selection quote support
|
|
123
123
|
this.handleMouseUp = this.handleMouseUp.bind(this)
|
|
124
|
-
|
|
124
|
+
document.addEventListener('mouseup', this.handleMouseUp)
|
|
125
125
|
|
|
126
126
|
this.currentUserId = document.body.dataset.currentUserId
|
|
127
127
|
const commentAuthorId = this.element.dataset.userId
|
|
@@ -223,7 +223,7 @@ export default class extends Controller {
|
|
|
223
223
|
clearTimeout(this._streamingTimeout)
|
|
224
224
|
this._streamingTimeout = null
|
|
225
225
|
}
|
|
226
|
-
|
|
226
|
+
document.removeEventListener('mouseup', this.handleMouseUp)
|
|
227
227
|
this.hideReviewPopup()
|
|
228
228
|
if (this._reviewPopupEl) {
|
|
229
229
|
this._reviewPopupEl.remove()
|