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
|
@@ -72,11 +72,12 @@ module Collavre
|
|
|
72
72
|
return delayed_decision(agent, :rate_limited, config)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
# Check 4: Topic concurrency limit (
|
|
75
|
+
# Check 4: Topic concurrency limit (scoped by creative to avoid cross-creative blocking)
|
|
76
76
|
topic_max = @policy_resolver.topic_max_concurrent_jobs
|
|
77
77
|
if topic_max && @context.key?("topic")
|
|
78
78
|
topic_id = @context.dig("topic", "id")
|
|
79
|
-
|
|
79
|
+
creative_id = @context.dig("creative", "id")
|
|
80
|
+
if (Task.running_for_topic(topic_id, creative_id).count + topic_immediate_count) >= topic_max
|
|
80
81
|
return deferred_decision(agent, :topic_concurrency)
|
|
81
82
|
end
|
|
82
83
|
end
|
|
@@ -74,7 +74,7 @@ module Collavre
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
# Drain the queue for the topic so waiting tasks can execute
|
|
77
|
-
AgentOrchestrator.dequeue_next_for_topic(task.topic_id)
|
|
77
|
+
AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
78
78
|
rescue StandardError => e
|
|
79
79
|
Rails.logger.error("[StuckDetector] Auto-recovery failed for task #{task.id}: #{e.message}")
|
|
80
80
|
end
|
|
@@ -6,6 +6,15 @@ module Collavre
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def dispatch(event_name, context)
|
|
9
|
+
comment_id = context.dig("comment", "id")
|
|
10
|
+
comment_user_id = context.dig("comment", "user_id")
|
|
11
|
+
creative_id = context.dig("creative", "id")
|
|
12
|
+
Rails.logger.info(
|
|
13
|
+
"[SystemEvents::Dispatcher] event=#{event_name} " \
|
|
14
|
+
"comment_id=#{comment_id} comment_user_id=#{comment_user_id} " \
|
|
15
|
+
"creative_id=#{creative_id} " \
|
|
16
|
+
"caller=#{caller_locations(1, 5)&.map { |l| "#{File.basename(l.path)}:#{l.lineno}" }&.join(' <- ')}"
|
|
17
|
+
)
|
|
9
18
|
# Delegate to AgentOrchestrator for unified routing/scheduling
|
|
10
19
|
Orchestration::AgentOrchestrator.dispatch(event_name, context)
|
|
11
20
|
end
|
|
@@ -5,6 +5,7 @@ module Tools
|
|
|
5
5
|
class CreativeCreateService
|
|
6
6
|
extend T::Sig
|
|
7
7
|
extend ToolMeta
|
|
8
|
+
include DescriptionNormalizable
|
|
8
9
|
|
|
9
10
|
tool_name "creative_create_service"
|
|
10
11
|
tool_description "Create a new Creative (task/content block) in the hierarchical structure. Creatives function like tasks in a tree structure, with automatic progress calculation.\n\nUse this to:\n- Create new tasks under a parent Creative\n- Add sub-items to organize work\n- Build hierarchical project structures\n\nNote: The description field accepts HTML format for rich text content."
|
|
@@ -62,14 +63,6 @@ module Tools
|
|
|
62
63
|
|
|
63
64
|
private
|
|
64
65
|
|
|
65
|
-
def normalize_description(desc)
|
|
66
|
-
return desc if desc.blank?
|
|
67
|
-
# If it already looks like HTML, return as-is
|
|
68
|
-
return desc if desc.strip.start_with?("<")
|
|
69
|
-
# Otherwise wrap in <p> tags
|
|
70
|
-
"<p>#{ERB::Util.html_escape(desc)}</p>"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
66
|
def handle_ordering(creative, before_id:, after_id:)
|
|
74
67
|
return unless before_id.present? || after_id.present?
|
|
75
68
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
require "sorbet-runtime"
|
|
3
|
+
require "rails_mcp_engine"
|
|
4
|
+
|
|
5
|
+
module Tools
|
|
6
|
+
class CreativeImportService
|
|
7
|
+
extend T::Sig
|
|
8
|
+
extend ToolMeta
|
|
9
|
+
|
|
10
|
+
tool_name "creative_import_service"
|
|
11
|
+
tool_description "Import a markdown document as a Creative tree structure using the built-in MarkdownImporter. " \
|
|
12
|
+
"Headings (# through ######) become nested Creatives, preserving the hierarchy. " \
|
|
13
|
+
"Bullet lists (-, *, +) become nested children with indent-based depth. " \
|
|
14
|
+
"Tables, fenced code blocks, and inline images are also supported.\n\n" \
|
|
15
|
+
"Example input:\n" \
|
|
16
|
+
"```\n# Project Plan\n## Phase 1\n- Setup infrastructure\n- Configure CI\n## Phase 2\n- Build features\n```\n\n" \
|
|
17
|
+
"This creates a tree under the specified parent Creative.\n\n" \
|
|
18
|
+
"This tool requires approval before execution."
|
|
19
|
+
|
|
20
|
+
def self.requires_approval?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
tool_param :markdown, description: "The markdown text to import. Headings and bullet lists define the tree structure.", required: true
|
|
25
|
+
tool_param :parent_id, description: "ID of the parent Creative to import under. The entire markdown tree will be created as children of this Creative.", required: true
|
|
26
|
+
|
|
27
|
+
sig { params(markdown: String, parent_id: Integer).returns(T::Hash[Symbol, T.untyped]) }
|
|
28
|
+
def call(markdown:, parent_id:)
|
|
29
|
+
raise "Current.user is required" unless Current.user
|
|
30
|
+
|
|
31
|
+
parent = Creative.find_by(id: parent_id)
|
|
32
|
+
return { error: "Parent Creative not found", id: parent_id } unless parent
|
|
33
|
+
return { error: "No write permission on parent Creative", id: parent_id } unless parent.has_permission?(Current.user, :write)
|
|
34
|
+
|
|
35
|
+
created = MarkdownImporter.import(markdown, parent: parent, user: Current.user)
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
success: true,
|
|
39
|
+
parent_id: parent_id,
|
|
40
|
+
created_count: created.size,
|
|
41
|
+
tree: created.map { |c| { id: c.id, description: c.description.to_s.truncate(100), parent_id: c.parent_id } }
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -7,131 +7,192 @@ module Tools
|
|
|
7
7
|
extend ToolMeta
|
|
8
8
|
|
|
9
9
|
tool_name "creative_retrieval_service"
|
|
10
|
-
tool_description "Retrieve creatives by ID or query text.
|
|
10
|
+
tool_description "Retrieve creatives by ID or query text. Without query or ID, it returns root creatives.\n\nA Creative is a content block that functions like a task, organized in a tree structure similar to a to-do list. You can navigate the tree at any level as a structured document, with progress automatically calculated to show what's been completed.\n\nDefault output is a compact markdown tree:\n<!-- format: [id] description (progress%) -->\n- [123] My Task (50%)\n - [124] Subtask A (100%)\n - [125] Subtask B (0%)\n\ne.g.\n- When user say creative or Test creative, it means \"Test\" creative and it's children as a writing page.\n- Summary of Test creative? - you need to search \"Test\" creatives with level 3 or more and find the title is \"Test\" or similar and make summary of that."
|
|
11
11
|
|
|
12
|
-
tool_param :id, description: "The ID of the creative to retrieve."
|
|
13
|
-
tool_param :query, description: "Text to search for in creative descriptions."
|
|
12
|
+
tool_param :id, description: "The ID of the creative to retrieve with its subtree."
|
|
13
|
+
tool_param :query, description: "Text to search for in creative descriptions and comments."
|
|
14
14
|
tool_param :level, description: "Creative tree depth to return (default: 3).", required: false
|
|
15
|
-
tool_param :
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
tool_param :tags, description: "Filter by tag names (comma-separated).", required: false
|
|
16
|
+
tool_param :progress_min, description: "Min progress filter (0.0-1.0).", required: false
|
|
17
|
+
tool_param :progress_max, description: "Max progress filter (0.0-1.0).", required: false
|
|
18
|
+
tool_param :updated_since, description: "ISO8601 timestamp - only items updated after this.", required: false
|
|
19
|
+
tool_param :include_comments, description: "Include recent comments per creative (default: false).", required: false
|
|
20
|
+
tool_param :format, description: "Response format: 'markdown' (default, compact tree) or 'json' (structured data with tags/dates).", required: false
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
params(
|
|
24
|
+
id: T.nilable(Integer),
|
|
25
|
+
query: T.nilable(String),
|
|
26
|
+
level: T.nilable(Integer),
|
|
27
|
+
tags: T.nilable(String),
|
|
28
|
+
progress_min: T.nilable(Float),
|
|
29
|
+
progress_max: T.nilable(Float),
|
|
30
|
+
updated_since: T.nilable(String),
|
|
31
|
+
include_comments: T.nilable(T::Boolean),
|
|
32
|
+
format: T.nilable(String)
|
|
33
|
+
).returns(T.any(String, T::Array[T::Hash[Symbol, T.untyped]]))
|
|
34
|
+
end
|
|
35
|
+
def call(id: nil, query: nil, level: 3, tags: nil, progress_min: nil, progress_max: nil, updated_since: nil, include_comments: false, format: "markdown")
|
|
19
36
|
level ||= 3
|
|
20
|
-
|
|
37
|
+
format ||= "markdown"
|
|
38
|
+
include_comments ||= false
|
|
39
|
+
|
|
40
|
+
raise "Current.user is required" unless Current.user
|
|
21
41
|
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
creatives = fetch_creatives(id: id, query: query)
|
|
43
|
+
creatives = apply_filters(creatives, tags: tags, progress_min: progress_min, progress_max: progress_max, updated_since: updated_since)
|
|
24
44
|
|
|
25
|
-
#
|
|
26
|
-
|
|
45
|
+
# Search queries return a flat list — no subtree expansion needed
|
|
46
|
+
if query.present? && id.blank?
|
|
47
|
+
creatives.map do |c|
|
|
48
|
+
{ id: c.id, description: Creatives::TreeFormatter.plain_description(c), progress: c.progress.to_f.round(2) }
|
|
49
|
+
end
|
|
50
|
+
elsif format == "json"
|
|
51
|
+
build_json_tree(creatives, depth: level, include_comments: include_comments)
|
|
52
|
+
else
|
|
53
|
+
formatter = Creatives::TreeFormatter.new(
|
|
54
|
+
max_depth: level - 1,
|
|
55
|
+
include_header: true,
|
|
56
|
+
include_comments: include_comments
|
|
57
|
+
)
|
|
58
|
+
formatter.format(creatives) + "\n"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
27
61
|
|
|
28
|
-
|
|
29
|
-
setup_controller(controller)
|
|
62
|
+
private
|
|
30
63
|
|
|
64
|
+
def fetch_creatives(id:, query:)
|
|
31
65
|
if id.present?
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
creative = Creative.find_by(id: id)
|
|
67
|
+
return [] unless creative && creative.has_permission?(Current.user, :read)
|
|
68
|
+
[ creative ]
|
|
69
|
+
elsif query.present?
|
|
70
|
+
search_creatives(query)
|
|
71
|
+
else
|
|
72
|
+
Creative.where(user: Current.user).roots.order(:sequence).to_a
|
|
73
|
+
end
|
|
74
|
+
end
|
|
35
75
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
76
|
+
def search_creatives(query)
|
|
77
|
+
sanitized = Creative.sanitize_sql_like(query)
|
|
78
|
+
pattern = "%#{sanitized}%"
|
|
39
79
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# index_result is expected to be a list of children or simple list
|
|
80
|
+
# Scope search to user's own + shared creatives via permission cache
|
|
81
|
+
accessible_ids = accessible_creative_ids
|
|
43
82
|
|
|
44
|
-
|
|
45
|
-
|
|
83
|
+
desc_ids = Creative.where(id: accessible_ids)
|
|
84
|
+
.where("description LIKE ?", pattern)
|
|
85
|
+
.pluck(:id)
|
|
46
86
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
comment_ids = Comment.where(creative_id: accessible_ids)
|
|
88
|
+
.where("content LIKE ?", pattern)
|
|
89
|
+
.pluck(:creative_id)
|
|
50
90
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
parent_node = filter_tree([ creative_details ]).first
|
|
54
|
-
parent_node[:children] = filtered_children
|
|
91
|
+
combined_ids = (desc_ids | comment_ids)
|
|
92
|
+
return [] if combined_ids.empty?
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Normal index call
|
|
59
|
-
result = dispatch_request(controller, :index, search: query, simple: simple, level: level, format: :json)
|
|
60
|
-
|
|
61
|
-
if result[:status] == 200
|
|
62
|
-
parsed = JSON.parse(result[:body], symbolize_names: true)
|
|
63
|
-
filter_result(parsed)
|
|
64
|
-
else
|
|
65
|
-
[ { error: "Controller returned status #{result[:status]}", body: result[:body] } ]
|
|
66
|
-
end
|
|
67
|
-
end
|
|
94
|
+
Creative.where(id: combined_ids)
|
|
95
|
+
.sort_by { |c| c.description.to_s.length }
|
|
68
96
|
end
|
|
69
97
|
|
|
70
|
-
|
|
98
|
+
def accessible_creative_ids
|
|
99
|
+
# User's own creatives + those shared via permission cache
|
|
100
|
+
own_ids = Creative.where(user: Current.user).pluck(:id)
|
|
101
|
+
shared_ids = CreativeSharesCache
|
|
102
|
+
.where(user_id: Current.user.id)
|
|
103
|
+
.where.not(permission: :no_access)
|
|
104
|
+
.pluck(:creative_id)
|
|
105
|
+
own_ids | shared_ids
|
|
106
|
+
end
|
|
71
107
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
def apply_filters(creatives, tags:, progress_min:, progress_max:, updated_since:)
|
|
109
|
+
result = creatives
|
|
110
|
+
|
|
111
|
+
if tags.present?
|
|
112
|
+
tag_names = tags.split(",").map(&:strip)
|
|
113
|
+
label_ids = Label.where(value: tag_names).pluck(:id)
|
|
114
|
+
tagged_ids = Tag.where(label_id: label_ids).pluck(:creative_id).to_set
|
|
115
|
+
# Batch: find which input creatives have tagged descendants (single query)
|
|
116
|
+
creative_ids = result.map(&:id)
|
|
117
|
+
ancestors_with_tagged = CreativeHierarchy
|
|
118
|
+
.where(ancestor_id: creative_ids)
|
|
119
|
+
.where(descendant_id: tagged_ids.to_a)
|
|
120
|
+
.pluck(:ancestor_id)
|
|
121
|
+
.to_set
|
|
122
|
+
result = result.select { |c| tagged_ids.include?(c.id) || ancestors_with_tagged.include?(c.id) }
|
|
77
123
|
end
|
|
78
|
-
end
|
|
79
124
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
125
|
+
if progress_min.present?
|
|
126
|
+
result = result.select { |c| c.progress.to_f >= progress_min.to_f }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if progress_max.present?
|
|
130
|
+
result = result.select { |c| c.progress.to_f <= progress_max.to_f }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if updated_since.present?
|
|
134
|
+
since = Time.parse(updated_since)
|
|
135
|
+
# Batch check: get all descendant IDs with updates, then filter
|
|
136
|
+
creative_ids = result.map(&:id)
|
|
137
|
+
updated_descendant_ancestors = CreativeHierarchy
|
|
138
|
+
.where(ancestor_id: creative_ids)
|
|
139
|
+
.joins("INNER JOIN creatives ON creatives.id = creative_hierarchies.descendant_id")
|
|
140
|
+
.where("creatives.updated_at >= ?", since)
|
|
141
|
+
.pluck(:ancestor_id)
|
|
142
|
+
.to_set
|
|
143
|
+
|
|
144
|
+
result = result.select { |c| c.updated_at >= since || updated_descendant_ancestors.include?(c.id) }
|
|
91
145
|
end
|
|
92
|
-
end
|
|
93
146
|
|
|
94
|
-
|
|
95
|
-
env = Rack::MockRequest.env_for(
|
|
96
|
-
"/creatives",
|
|
97
|
-
method: "GET",
|
|
98
|
-
params: params.compact,
|
|
99
|
-
"HTTP_X_ORIGIN_SECRET" => ENV["ORIGIN_SHARED_SECRET"] # Internal call
|
|
100
|
-
)
|
|
101
|
-
controller.request = ActionDispatch::Request.new(env)
|
|
102
|
-
controller.response = ActionDispatch::Response.new
|
|
103
|
-
controller.process(action)
|
|
104
|
-
|
|
105
|
-
{ status: controller.response.status, body: controller.response.body }
|
|
147
|
+
result
|
|
106
148
|
end
|
|
107
149
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
else
|
|
116
|
-
[]
|
|
150
|
+
# --- JSON format ---
|
|
151
|
+
|
|
152
|
+
def build_json_tree(creatives, depth:, include_comments: false)
|
|
153
|
+
return [] if creatives.blank?
|
|
154
|
+
|
|
155
|
+
creatives.map do |creative|
|
|
156
|
+
serialize_creative(creative, depth: depth, current_depth: 1, include_comments: include_comments)
|
|
117
157
|
end
|
|
118
158
|
end
|
|
119
159
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
160
|
+
def serialize_creative(creative, depth:, current_depth:, include_comments: false)
|
|
161
|
+
children = creative.linked_children
|
|
162
|
+
|
|
163
|
+
result = {
|
|
164
|
+
id: creative.id,
|
|
165
|
+
description: Creatives::TreeFormatter.plain_description(creative),
|
|
166
|
+
progress: creative.progress.to_f.round(2),
|
|
167
|
+
parent_id: creative.parent_id,
|
|
168
|
+
tags: creative.tags.includes(:label).map { |t| t.label&.value }.compact,
|
|
169
|
+
linked: creative.origin_id.present?,
|
|
170
|
+
origin_id: creative.origin_id,
|
|
171
|
+
has_children: children.any?,
|
|
172
|
+
children_count: children.size,
|
|
173
|
+
created_at: creative.created_at&.iso8601,
|
|
174
|
+
updated_at: creative.updated_at&.iso8601
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if include_comments
|
|
178
|
+
result[:recent_comments] = creative.comments.order(created_at: :desc).limit(3).map do |comment|
|
|
179
|
+
{
|
|
180
|
+
content: ActionView::Base.full_sanitizer.sanitize(comment.content).strip.truncate(200),
|
|
181
|
+
user: comment.user&.display_name || comment.user&.name,
|
|
182
|
+
created_at: comment.created_at&.iso8601
|
|
183
|
+
}
|
|
126
184
|
end
|
|
185
|
+
end
|
|
127
186
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
187
|
+
if current_depth < depth
|
|
188
|
+
result[:children] = children.map do |child|
|
|
189
|
+
serialize_creative(child, depth: depth, current_depth: current_depth + 1, include_comments: include_comments)
|
|
190
|
+
end
|
|
191
|
+
else
|
|
192
|
+
result[:children] = []
|
|
134
193
|
end
|
|
194
|
+
|
|
195
|
+
result
|
|
135
196
|
end
|
|
136
197
|
end
|
|
137
198
|
end
|
|
@@ -5,6 +5,7 @@ module Tools
|
|
|
5
5
|
class CreativeUpdateService
|
|
6
6
|
extend T::Sig
|
|
7
7
|
extend ToolMeta
|
|
8
|
+
include DescriptionNormalizable
|
|
8
9
|
|
|
9
10
|
tool_name "creative_update_service"
|
|
10
11
|
tool_description "Update an existing Creative's content, progress, or parent. Use this to:\n- Modify the description/title of a Creative\n- Mark a leaf Creative as complete (progress = 1.0)\n- Move a Creative to a different parent\n\nProgress constraints:\n- Only 1.0 (100%) is allowed — partial progress updates are not supported\n- Only leaf Creatives (with no children) can have their progress updated\n- Parent Creative progress is automatically calculated from children\n\nUse creative_retrieval_service to find the correct Creative before updating."
|
|
@@ -108,14 +109,6 @@ module Tools
|
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
private
|
|
111
|
-
|
|
112
|
-
def normalize_description(desc)
|
|
113
|
-
return desc if desc.blank?
|
|
114
|
-
# If it already looks like HTML, return as-is
|
|
115
|
-
return desc if desc.strip.start_with?("<")
|
|
116
|
-
# Otherwise wrap in <p> tags
|
|
117
|
-
"<p>#{ERB::Util.html_escape(desc)}</p>"
|
|
118
|
-
end
|
|
119
112
|
end
|
|
120
113
|
end
|
|
121
114
|
end
|
|
@@ -23,7 +23,7 @@ module Tools
|
|
|
23
23
|
unless creative.has_permission?(Current.user, :read)
|
|
24
24
|
return { error: "No read permission on this Creative", id: creative_id }
|
|
25
25
|
end
|
|
26
|
-
tasks = tasks.where("key LIKE ?", "cron_#{creative_id}_%")
|
|
26
|
+
tasks = tasks.where("key LIKE ?", "cron_#{creative_id.to_i}_%")
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
results = tasks.filter_map do |task|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Tools
|
|
3
|
+
module DescriptionNormalizable
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def normalize_description(desc)
|
|
7
|
+
return desc if desc.blank?
|
|
8
|
+
stripped = desc.strip
|
|
9
|
+
# If it already looks like HTML, return as-is
|
|
10
|
+
return stripped if stripped.start_with?("<")
|
|
11
|
+
# Otherwise wrap in <p> tags
|
|
12
|
+
"<p>#{ERB::Util.html_escape(stripped)}</p>"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -57,26 +57,43 @@
|
|
|
57
57
|
<%= t('collavre.comments.edit_button') %>
|
|
58
58
|
</button>
|
|
59
59
|
<% if comment.user&.ai_user? %>
|
|
60
|
-
<button class="review-comment-btn" data-
|
|
60
|
+
<button class="review-comment-btn" data-action="click->comment#reviewClick" data-comment-id="<%= comment.id %>" title="<%= t('collavre.comments.review_button') %>" data-hint-text="<%= t('collavre.comments.review_select_hint') %>">
|
|
61
61
|
<%= t('collavre.comments.review_button') %>
|
|
62
62
|
</button>
|
|
63
|
-
<button class="replace-comment-btn" data-comment-target="replaceButton" data-comment-id="<%= comment.id %>" title="<%= t('collavre.comments.replace_button') %>" disabled>
|
|
64
|
-
<%= t('collavre.comments.replace_button') %>
|
|
65
|
-
</button>
|
|
66
63
|
<% end %>
|
|
67
64
|
<button class="copy-comment-link-btn" data-comment-id="<%= comment.id %>" data-comment-url="<%= collavre.creative_comment_url(comment.creative, comment, Rails.application.config.action_mailer.default_url_options) %>" title="<%= t('collavre.comments.copy_link_button') %>">
|
|
68
65
|
<%= t('collavre.comments.copy_link_button') %>
|
|
69
66
|
</button>
|
|
70
|
-
<button class="delete-comment-btn comment-delete-hidden" data-comment-target="deleteButton" data-comment-id="<%= comment.id %>" title="<%= t('collavre.comments.delete') %>">
|
|
71
|
-
<%= t('collavre.comments.delete_button') %>
|
|
72
|
-
</button>
|
|
73
67
|
</div>
|
|
74
68
|
<% if comment.quoted_text.present? %>
|
|
75
69
|
<div class="comment-quoted-block">
|
|
76
70
|
<blockquote class="comment-quoted-text"><%= comment.quoted_text %></blockquote>
|
|
77
71
|
</div>
|
|
78
72
|
<% end %>
|
|
79
|
-
<div class="comment-content"><%= comment.content %></div>
|
|
73
|
+
<div class="comment-content" data-comment-target="content"><%= comment.content %></div>
|
|
74
|
+
<% if comment.comment_versions.any? %>
|
|
75
|
+
<% version_count = comment.comment_versions.size %>
|
|
76
|
+
<% selected_id = comment.selected_version_id %>
|
|
77
|
+
<% version_ids = comment.comment_versions.order(:version_number).pluck(:id) %>
|
|
78
|
+
<% selected_index = selected_id ? (version_ids.index(selected_id)&.+(1) || version_count) : version_count %>
|
|
79
|
+
<div class="comment-version-navigator"
|
|
80
|
+
data-controller="comment-version"
|
|
81
|
+
data-comment-version-comment-id-value="<%= comment.id %>"
|
|
82
|
+
data-comment-version-creative-id-value="<%= comment.creative_id %>"
|
|
83
|
+
data-comment-version-versions-url-value="<%= collavre.creative_comment_versions_path(comment.creative, comment) %>"
|
|
84
|
+
data-comment-version-total-value="<%= version_count %>"
|
|
85
|
+
data-comment-version-initial-index-value="<%= selected_index %>"
|
|
86
|
+
data-comment-version-selected-version-id-value="<%= selected_id %>"
|
|
87
|
+
data-comment-version-content-target-value="<%= dom_id(comment) %>">
|
|
88
|
+
<button class="comment-version-btn" data-action="click->comment-version#prev" data-comment-version-target="prevBtn" title="<%= t('collavre.comments.versions.previous') %>">◀</button>
|
|
89
|
+
<span class="comment-version-indicator" data-comment-version-target="indicator">
|
|
90
|
+
v<%= selected_index %>/<%= version_count %>
|
|
91
|
+
</span>
|
|
92
|
+
<button class="comment-version-btn" data-action="click->comment-version#next" data-comment-version-target="nextBtn" title="<%= t('collavre.comments.versions.next') %>">▶</button>
|
|
93
|
+
<button class="comment-version-select-btn" data-action="click->comment-version#selectVersion" data-comment-version-target="selectBtn" title="<%= t('collavre.comments.versions.select') %>" disabled>✓ <%= t('collavre.comments.versions.select') %></button>
|
|
94
|
+
<button class="comment-version-delete-btn" data-action="click->comment-version#deleteVersion" data-comment-version-target="deleteBtn" data-confirm-message="<%= t('collavre.comments.versions.delete_confirm') %>" title="<%= t('collavre.comments.versions.delete') %>" disabled>✕</button>
|
|
95
|
+
</div>
|
|
96
|
+
<% end %>
|
|
80
97
|
|
|
81
98
|
|
|
82
99
|
<% if comment.images.attached? %>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<% fullscreen = local_assigns.fetch(:fullscreen, false) %>
|
|
3
3
|
<% auto_fullscreen = local_assigns.fetch(:auto_fullscreen, false) %>
|
|
4
4
|
<% creative = local_assigns[:creative] %>
|
|
5
|
-
<div id="comments-popup" data-controller="comments--popup comments--list comments--form comments--presence comments--mention-menu comments--topics" class="popup-box"
|
|
5
|
+
<div id="comments-popup" data-controller="comments--popup comments--list comments--form comments--presence comments--mention-menu comments--topics comments--contexts share-modal" class="popup-box"
|
|
6
6
|
data-fullscreen="<%= fullscreen %>"
|
|
7
7
|
<% if auto_fullscreen %>data-auto-fullscreen="true"<% end %>
|
|
8
8
|
data-fullscreen-label="<%= t('collavre.comments.fullscreen', default: 'Full screen') %>"
|
|
@@ -29,16 +29,38 @@
|
|
|
29
29
|
data-voice-stop-text="<%= t('collavre.comments.voice_stop') %>"
|
|
30
30
|
data-move-no-selection-text="<%= t('collavre.comments.move_no_selection') %>"
|
|
31
31
|
data-move-error-text="<%= t('collavre.comments.move_error') %>"
|
|
32
|
-
data-
|
|
33
|
-
data-
|
|
32
|
+
data-selection-count-text="<%= t('collavre.comments.selection_count') %>"
|
|
33
|
+
data-selection-delete-text="<%= t('collavre.comments.selection_delete') %>"
|
|
34
|
+
data-selection-move-text="<%= t('collavre.comments.selection_move') %>"
|
|
35
|
+
data-selection-topic-move-text="<%= t('collavre.comments.selection_topic_move') %>"
|
|
36
|
+
data-selection-close-text="<%= t('collavre.comments.selection_close') %>"
|
|
37
|
+
data-selection-drag-hint-text="<%= t('collavre.comments.selection_drag_hint') %>"
|
|
38
|
+
data-batch-delete-confirm-text="<%= t('collavre.comments.batch_delete_confirm') %>"
|
|
39
|
+
data-topic-search-placeholder-text="<%= t('collavre.comments.topic_search_placeholder') %>"
|
|
40
|
+
data-topic-main-text="<%= t('collavre.comments.topic_main') %>"
|
|
34
41
|
data-add-participant-text="<%= t('collavre.comments.add_participant') %>"
|
|
35
|
-
data-review-button-text="<%= t('collavre.comments.review_button') %>"
|
|
42
|
+
data-review-button-text="<%= t('collavre.comments.review_button') %>"
|
|
43
|
+
data-review-feedback-placeholder="<%= t('collavre.comments.review_feedback_placeholder') %>"
|
|
44
|
+
data-review-summary-placeholder="<%= t('collavre.comments.review_summary_placeholder') %>"
|
|
45
|
+
data-review-add-quote="<%= t('collavre.comments.review_add_quote') %>"
|
|
46
|
+
data-review-send="<%= t('collavre.comments.review_send') %>"
|
|
47
|
+
data-review-send-question="<%= t('collavre.comments.review_send_question') %>"
|
|
48
|
+
data-review-type-review="💬"
|
|
49
|
+
data-review-type-question="❓"
|
|
50
|
+
data-review-type-review-label="<%= t('collavre.comments.review_type_review') %>"
|
|
51
|
+
data-review-type-question-label="<%= t('collavre.comments.review_type_question') %>">
|
|
36
52
|
<div class="resize-handle resize-handle-left" data-comments--popup-target="leftHandle"></div>
|
|
37
53
|
<div class="resize-handle resize-handle-right" data-comments--popup-target="rightHandle"></div>
|
|
38
54
|
<div class="comments-popup-header">
|
|
39
55
|
<h3 id="comments-popup-title" data-comments--popup-target="title"><%= fullscreen && creative.present? ? creative.creative_snippet : t('collavre.comments.comments') %></h3>
|
|
40
56
|
<span data-integration-badges class="integration-badges"></span>
|
|
41
57
|
<div class="comments-popup-actions">
|
|
58
|
+
<button class="comments-popup-action context-toggle-btn"
|
|
59
|
+
data-comments--contexts-target="toggleButton"
|
|
60
|
+
data-action="click->comments--contexts#toggleVisibility"
|
|
61
|
+
type="button"
|
|
62
|
+
title="<%= t('collavre.contexts.toggle_label', default: 'Contexts') %>"
|
|
63
|
+
style="display:none;">🔗</button>
|
|
42
64
|
<button class="comments-popup-action comments-popup-fullscreen"
|
|
43
65
|
data-comments--popup-target="fullscreenButton"
|
|
44
66
|
data-action="click->comments--popup#toggleFullscreen"
|
|
@@ -54,6 +76,11 @@
|
|
|
54
76
|
<button id="close-comments-btn" data-comments--popup-target="closeButton" class="popup-close-btn" type="button">×</button>
|
|
55
77
|
</div>
|
|
56
78
|
</div>
|
|
79
|
+
<div id="comment-contexts" data-comments--contexts-target="list" class="comment-contexts-list" style="display:none;"
|
|
80
|
+
data-inherited-label="<%= t('collavre.contexts.inherited_label', default: 'Inherited from parent') %>"
|
|
81
|
+
data-self-context-label="<%= t('collavre.contexts.self_context_label', default: 'Current creative context') %>"
|
|
82
|
+
data-navigate-label="<%= t('collavre.contexts.navigate_label', default: 'Go to creative') %>"></div>
|
|
83
|
+
<div data-share-modal-target="container"></div>
|
|
57
84
|
<div id="comment-participants" data-comments--presence-target="participants" data-comments--mention-menu-target="participants"></div>
|
|
58
85
|
<div id="comment-topics" data-comments--topics-target="list" class="comment-topics-list"
|
|
59
86
|
data-confirm-delete-text="<%= t('collavre.topics.delete_confirm') %>"
|
|
@@ -69,6 +96,7 @@
|
|
|
69
96
|
<span class="comment-quote-indicator-text" data-comments--form-target="quoteIndicatorText"></span>
|
|
70
97
|
<button type="button" class="comment-quote-cancel" data-action="click->comments--form#cancelQuote" title="<%= t('app.cancel') %>">×</button>
|
|
71
98
|
</div>
|
|
99
|
+
<div class="review-quotes-container" data-comments--form-target="reviewQuotesContainer" style="display:none;"></div>
|
|
72
100
|
<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>
|
|
73
101
|
<div class="comment-bottom">
|
|
74
102
|
<input type="file" id="comment-images" name="comment[images][]" accept="image/*" multiple data-comments--form-target="imageInput" style="display:none;" />
|
|
@@ -77,7 +105,6 @@
|
|
|
77
105
|
<button class="creative-action-btn" id="attach-image-btn" data-comments--form-target="imageButton" type="button"><%= t('collavre.comments.image_button') %></button>
|
|
78
106
|
<button class="creative-action-btn" id="voice-comments-btn" data-comments--form-target="voiceButton" type="button" data-voice-state="idle"><%= t('collavre.comments.voice_button') %></button>
|
|
79
107
|
<button class="creative-action-btn" id="search-comments-btn" data-comments--form-target="searchButton" type="button"><%= t('collavre.comments.search_button') %></button>
|
|
80
|
-
<button class="creative-action-btn" id="move-comments-btn" data-comments--form-target="moveButton" type="button" disabled><%= t('collavre.comments.move_button') %></button>
|
|
81
108
|
<button class="creative-action-btn" id="cancel-edit-btn" data-comments--form-target="cancel" type="button" style="display:none;"><%= t('app.cancel') %></button>
|
|
82
109
|
<button class="creative-action-btn" type="submit" data-comments--form-target="submit"><%= svg_tag 'send.svg', class: 'send-icon' %></button>
|
|
83
110
|
</div>
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
data-children-alert-message="<%= t('collavre.creatives.index.progress_complete_children_alert') %>">
|
|
28
28
|
<span id="inline-progress-value"><%= t('collavre.creatives.index.progress_incomplete') %></span>
|
|
29
29
|
</label>
|
|
30
|
+
<button type="button" id="inline-metadata-btn" class="creative-action-btn" title="<%= t('collavre.creatives.index.metadata_tooltip') %>" style="margin-left:0.5em;font-family:monospace;font-size:0.9em;">
|
|
31
|
+
{ }
|
|
32
|
+
</button>
|
|
30
33
|
</div>
|
|
31
34
|
<div style="margin-top:0.5em;">
|
|
32
35
|
<button type="button" id="inline-move-up" class="creative-action-btn" title="<%= t('collavre.creatives.index.inline_move_up_tooltip') %>">
|
|
@@ -91,6 +94,16 @@
|
|
|
91
94
|
</div>
|
|
92
95
|
<button type="button" id="inline-recommend-parent" class="creative-action-btn" style="margin-top:0.5em;" title="<%= t('collavre.creatives.index.recommend_parent') %>">✨<%= t('collavre.creatives.index.recommend_parent') %></button>
|
|
93
96
|
<select id="parent-suggestions" size="5" style="display:none;margin-top:0.5em;width:100%;"></select>
|
|
97
|
+
<div id="metadata-popup" style="display:none;" class="metadata-popup">
|
|
98
|
+
<div class="metadata-popup-header">
|
|
99
|
+
<span><%= t('collavre.creatives.index.metadata_title') %></span>
|
|
100
|
+
<button type="button" id="metadata-popup-close">×</button>
|
|
101
|
+
</div>
|
|
102
|
+
<textarea id="metadata-yaml-editor" rows="12"></textarea>
|
|
103
|
+
<button type="button" id="metadata-save-btn" class="creative-action-btn">
|
|
104
|
+
<%= t('collavre.creatives.index.metadata_save') %>
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
94
107
|
</div>
|
|
95
108
|
</form>
|
|
96
109
|
</div>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
<% current_creative = @parent_creative || @creative %>
|
|
2
|
+
<button id="share-creative-btn" class="btn btn-primary desktop-only"
|
|
3
|
+
data-action="click->share-modal#open"
|
|
4
|
+
data-share-modal-url-param="<%= collavre.creative_creative_shares_path(current_creative) %>">
|
|
2
5
|
<span aria-hidden="true"><%= svg_tag 'share.svg', class: 'icon-up', width: 22, height: 20 %></span>
|
|
3
6
|
</button>
|