collavre 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comment_versions.css +76 -0
  3. data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
  4. data/app/assets/stylesheets/collavre/creatives.css +73 -1
  5. data/app/assets/stylesheets/collavre/org_chart.css +319 -0
  6. data/app/assets/stylesheets/collavre/popup.css +68 -1
  7. data/app/controllers/collavre/application_controller.rb +13 -0
  8. data/app/controllers/collavre/comments/versions_controller.rb +82 -0
  9. data/app/controllers/collavre/comments_controller.rb +14 -153
  10. data/app/controllers/collavre/concerns/exportable.rb +30 -0
  11. data/app/controllers/collavre/concerns/shareable.rb +28 -0
  12. data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
  13. data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
  14. data/app/controllers/collavre/creative_imports_controller.rb +6 -0
  15. data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
  16. data/app/controllers/collavre/creative_plans_controller.rb +1 -1
  17. data/app/controllers/collavre/creative_shares_controller.rb +84 -14
  18. data/app/controllers/collavre/creatives_controller.rb +70 -194
  19. data/app/controllers/collavre/google_auth_controller.rb +3 -0
  20. data/app/controllers/collavre/invites_controller.rb +2 -1
  21. data/app/controllers/collavre/sessions_controller.rb +3 -0
  22. data/app/controllers/collavre/topics_controller.rb +39 -2
  23. data/app/controllers/collavre/users_controller.rb +5 -404
  24. data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
  25. data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
  26. data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
  27. data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
  28. data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
  29. data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
  30. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
  31. data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
  32. data/app/helpers/collavre/application_helper.rb +1 -0
  33. data/app/helpers/collavre/creatives_helper.rb +12 -9
  34. data/app/helpers/collavre/navigation_helper.rb +1 -1
  35. data/app/javascript/collavre.js +0 -1
  36. data/app/javascript/controllers/comment_controller.js +33 -70
  37. data/app/javascript/controllers/comment_version_controller.js +164 -0
  38. data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
  39. data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
  40. data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
  41. data/app/javascript/controllers/comments/contexts_controller.js +363 -0
  42. data/app/javascript/controllers/comments/form_controller.js +304 -13
  43. data/app/javascript/controllers/comments/list_controller.js +151 -62
  44. data/app/javascript/controllers/comments/popup_controller.js +66 -38
  45. data/app/javascript/controllers/comments/presence_controller.js +2 -10
  46. data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
  47. data/app/javascript/controllers/comments/topics_controller.js +34 -10
  48. data/app/javascript/controllers/index.js +15 -1
  49. data/app/javascript/controllers/org_chart_controller.js +46 -0
  50. data/app/javascript/controllers/share_modal_controller.js +369 -0
  51. data/app/javascript/controllers/topic_search_controller.js +103 -0
  52. data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
  53. data/app/javascript/lib/api/creatives.js +12 -0
  54. data/app/javascript/lib/api/csrf_fetch.js +35 -0
  55. data/app/javascript/lib/api/drag_drop.js +17 -0
  56. data/app/javascript/modules/command_menu.js +40 -0
  57. data/app/javascript/modules/creative_row_editor.js +88 -0
  58. data/app/javascript/modules/slide_view.js +2 -1
  59. data/app/jobs/collavre/ai_agent_job.rb +42 -30
  60. data/app/jobs/collavre/compress_job.rb +92 -0
  61. data/app/models/collavre/comment.rb +36 -1
  62. data/app/models/collavre/comment_version.rb +15 -0
  63. data/app/models/collavre/creative/describable.rb +1 -1
  64. data/app/models/collavre/creative.rb +51 -0
  65. data/app/models/collavre/task.rb +30 -2
  66. data/app/models/collavre/user.rb +20 -3
  67. data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
  68. data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
  69. data/app/services/collavre/ai_agent/message_builder.rb +85 -6
  70. data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
  71. data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
  72. data/app/services/collavre/ai_agent/review_handler.rb +18 -1
  73. data/app/services/collavre/ai_agent_service.rb +130 -183
  74. data/app/services/collavre/ai_client.rb +6 -0
  75. data/app/services/collavre/auto_theme_generator.rb +1 -1
  76. data/app/services/collavre/command_menu_service.rb +19 -0
  77. data/app/services/collavre/comments/command_processor.rb +3 -1
  78. data/app/services/collavre/comments/compress_command.rb +75 -0
  79. data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
  80. data/app/services/collavre/comments/work_command.rb +161 -0
  81. data/app/services/collavre/comments/workflow_executor.rb +276 -0
  82. data/app/services/collavre/creatives/plan_tagger.rb +14 -3
  83. data/app/services/collavre/creatives/tree_formatter.rb +53 -13
  84. data/app/services/collavre/gemini_parent_recommender.rb +4 -4
  85. data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
  86. data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
  87. data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
  88. data/app/services/collavre/orchestration/scheduler.rb +3 -2
  89. data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
  90. data/app/services/collavre/system_events/dispatcher.rb +9 -0
  91. data/app/services/collavre/tools/creative_create_service.rb +1 -8
  92. data/app/services/collavre/tools/creative_import_service.rb +46 -0
  93. data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
  94. data/app/services/collavre/tools/creative_update_service.rb +1 -8
  95. data/app/services/collavre/tools/cron_list_service.rb +1 -1
  96. data/app/services/collavre/tools/description_normalizable.rb +16 -0
  97. data/app/views/collavre/comments/_comment.html.erb +25 -8
  98. data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
  99. data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
  100. data/app/views/collavre/creatives/_share_button.html.erb +4 -1
  101. data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
  102. data/app/views/collavre/creatives/index.html.erb +5 -5
  103. data/app/views/collavre/creatives/slide_view.html.erb +1 -1
  104. data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
  105. data/app/views/collavre/users/_org_chart.html.erb +68 -0
  106. data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
  107. data/app/views/collavre/users/new_ai.html.erb +9 -0
  108. data/app/views/collavre/users/show.html.erb +32 -8
  109. data/config/locales/comments.en.yml +57 -2
  110. data/config/locales/comments.ko.yml +57 -2
  111. data/config/locales/contacts.en.yml +31 -0
  112. data/config/locales/contacts.ko.yml +31 -0
  113. data/config/locales/contexts.en.yml +8 -0
  114. data/config/locales/contexts.ko.yml +8 -0
  115. data/config/locales/creatives.en.yml +6 -0
  116. data/config/locales/creatives.ko.yml +6 -0
  117. data/config/locales/users.en.yml +1 -0
  118. data/config/locales/users.ko.yml +1 -0
  119. data/config/routes.rb +14 -1
  120. data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
  121. data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
  122. data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
  123. data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
  124. data/lib/collavre/version.rb +1 -1
  125. metadata +47 -10
  126. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
  127. data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
  128. data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
  129. data/app/javascript/modules/share_modal.js +0 -76
  130. data/app/javascript/modules/share_user_popup.js +0 -77
  131. data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
  132. data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
  133. data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
@@ -1,5 +1,5 @@
1
1
  <!-- Share Creative Modal -->
2
- <div id="share-creative-modal" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10000;align-items:center;justify-content:center;" data-creative-id="<%= (@parent_creative || @creative).id %>">
2
+ <div id="share-creative-modal" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10000;align-items:center;justify-content:center;" data-creative-id="<%= (@parent_creative || @creative).id %>" data-error-message="<%= t('collavre.creatives.share.network_error') %>">
3
3
  <div class="popup-box" style="min-width:320px;max-width:90vw;">
4
4
  <button type="button" id="close-share-modal" class="popup-close-btn">&times;</button>
5
5
  <h2><%= t('collavre.creatives.index.share_creative') %></h2>
@@ -48,5 +48,35 @@
48
48
  </ul>
49
49
  </div>
50
50
  <% end %>
51
+ <% if defined?(@pending_invitations) && @pending_invitations.any? %>
52
+ <div style="margin-top:1em;">
53
+ <strong><%= t('collavre.contacts.org_chart.pending_invitations') %>:</strong>
54
+ <ul class="share-grid">
55
+ <% @pending_invitations.each do |invitation| %>
56
+ <li>
57
+ <span>
58
+ <div class="avatar-wrapper" style="width: 20px; height: 20px; display: inline-block;">
59
+ <% if invitation.email.present? %>
60
+ <%= image_tag asset_path("default_avatar.svg"),
61
+ alt: '', width: 20, height: 20,
62
+ class: 'avatar share-avatar',
63
+ title: invitation.email,
64
+ style: 'border-radius:50%;vertical-align:middle;' %>
65
+ <span class="avatar-initial" style="font-size: 10px;"><%= invitation.email[0]&.upcase %></span>
66
+ <% else %>
67
+ <span class="avatar share-avatar" style="display:inline-block;width:20px;height:20px;border-radius:50%;background:var(--surface-3,#ddd);text-align:center;line-height:20px;font-size:10px;">🔗</span>
68
+ <% end %>
69
+ </div>
70
+ </span>
71
+ <span><%= invitation.email.presence || t('collavre.creatives.share.public_invite') %></span>
72
+ <span><%= t("collavre.creatives.index.permission_#{invitation.permission}") %></span>
73
+ <span>
74
+ <%= button_to '×', collavre.creative_invitation_path(@parent_creative || @creative, invitation), method: :delete, form: { data: { turbo_confirm: t('collavre.contacts.org_chart.cancel_invite_confirm', email: invitation.email.presence || t('collavre.creatives.share.public_invite')) } }, class: 'delete-share-btn' %>
75
+ </span>
76
+ </li>
77
+ <% end %>
78
+ </ul>
79
+ </div>
80
+ <% end %>
51
81
  </div>
52
82
  </div>
@@ -1,6 +1,6 @@
1
1
  <%= tag.div(flash[:alert], class: "flash-alert") if flash[:alert] %>
2
2
 
3
- <div data-controller="creatives--import creatives--select-mode creatives--drag-drop creatives--expansion creatives--row-editor creatives--set-plan-modal"
3
+ <div data-controller="creatives--import creatives--select-mode creatives--drag-drop creatives--expansion creatives--row-editor creatives--set-plan-modal share-modal"
4
4
  data-creatives--import-parent-id-value="<%= @parent_creative&.id %>"
5
5
  data-creatives--import-uploading-value="<%= t('collavre.creatives.index.import_uploading') %>"
6
6
  data-creatives--import-success-value="<%= t('collavre.creatives.index.import_success') %>"
@@ -61,9 +61,9 @@
61
61
  <%= render 'mobile_actions_menu', current_creative: current_creative, can_manage_integrations: can_manage_integrations %>
62
62
  </div>
63
63
 
64
- <% if (@parent_creative || @creative) && (@parent_creative || @creative).has_permission?(Current.user, :write) %>
65
- <%= render 'share_modal' %>
66
- <% end %>
64
+ <% if (@parent_creative || @creative) && (@parent_creative || @creative).has_permission?(Current.user, :write) %>
65
+ <div data-share-modal-target="container"></div>
66
+ <% end %>
67
67
 
68
68
  <%= render 'import_upload_zone' %>
69
69
 
@@ -151,7 +151,7 @@
151
151
  end +
152
152
  content_tag(:template, data: { part: "progress" }) do
153
153
  if @overall_progress.nil?
154
- "".html_safe
154
+ safe_join([])
155
155
  else
156
156
  render_progress_value(@overall_progress)
157
157
  end
@@ -8,7 +8,7 @@
8
8
  <% initial_index = params[:slide].to_i.clamp(0, @slide_ids.length - 1) %>
9
9
  <% initial_prompt = @creative.prompt_for(Current.user) %>
10
10
  <div id="slide-view" data-slide-ids="<%= @slide_ids.join(',') %>" data-initial-index="<%= initial_index %>" data-root-id="<%= @creative.id %>">
11
- <div id="slide-content"><%= content_tag(tag_name, @creative.effective_description.html_safe) %></div>
11
+ <div id="slide-content"><%= content_tag(tag_name, sanitize(@creative.effective_description, tags: Rails::HTML5::SafeListSanitizer.allowed_tags.to_a + %w[table thead tbody tfoot tr th td], attributes: Rails::HTML5::SafeListSanitizer.allowed_attributes.to_a + %w[colspan rowspan data-lexical])) %></div>
12
12
  </div>
13
13
  <div id="slide-controls">
14
14
  <div id="slide-counter"><%= initial_index + 1 %> / <%= @slide_ids.length %></div>
@@ -1,10 +1,5 @@
1
1
  <div class="contact-management">
2
- <div class="flex-row-center-between">
3
- <p class="text-muted"><%= t('collavre.contacts.description') %></p>
4
- <%= link_to t('collavre.users.add_ai_user'), collavre.new_ai_users_path, class: "primary-action-button" %>
5
- </div>
6
-
7
- <% show_actions = (contacts || []).any? { |c| c.contact_user.ai_user? && c.contact_user.created_by_id == Current.user.id } %>
2
+ <% show_actions = (contacts || []).any? { |c| c.contact_user&.ai_user? && c.contact_user&.created_by_id == Current.user.id } %>
8
3
 
9
4
  <% if contacts.present? %>
10
5
  <div class="table-scroll">
@@ -24,6 +19,7 @@
24
19
  <tbody>
25
20
  <% contacts.each do |contact| %>
26
21
  <% contact_user = contact.contact_user %>
22
+ <% next unless contact_user %>
27
23
  <tr>
28
24
  <td>
29
25
  <%= render Collavre::AvatarComponent.new(
@@ -67,11 +63,11 @@
67
63
  <% prev_page = contact_page.to_i - 1 %>
68
64
  <% next_page = contact_page.to_i + 1 %>
69
65
  <% if contact_page.to_i > 1 %>
70
- <%= link_to t('collavre.contacts.pagination.prev'), collavre.user_path(Current.user, tab: 'contacts', contact_page: prev_page), class: 'pagination-button' %>
66
+ <%= link_to t('collavre.contacts.pagination.prev'), collavre.user_path(Current.user, tab: 'contacts', contacts_view: 'list', contact_page: prev_page), class: 'pagination-button' %>
71
67
  <% end %>
72
68
  <span class="pagination-status"><%= t('collavre.contacts.pagination.page_info', current: contact_page, total: total_contact_pages) %></span>
73
69
  <% if contact_page.to_i < total_contact_pages.to_i %>
74
- <%= link_to t('collavre.contacts.pagination.next'), collavre.user_path(Current.user, tab: 'contacts', contact_page: next_page), class: 'pagination-button' %>
70
+ <%= link_to t('collavre.contacts.pagination.next'), collavre.user_path(Current.user, tab: 'contacts', contacts_view: 'list', contact_page: next_page), class: 'pagination-button' %>
75
71
  <% end %>
76
72
  </nav>
77
73
  <% end %>
@@ -0,0 +1,68 @@
1
+ <div class="org-chart" data-controller="org-chart share-modal">
2
+ <%# Container for dynamically loaded share modal %>
3
+ <div data-share-modal-target="container"></div>
4
+
5
+ <% if org_chart_roots.present? %>
6
+ <div class="org-chart-toolbar">
7
+ <button type="button" class="btn btn-xs btn-secondary"
8
+ data-action="click->org-chart#toggleAll"
9
+ data-org-chart-target="toggleAllBtn"
10
+ data-expand-text="<%= t('collavre.contacts.org_chart.expand_all') %>"
11
+ data-collapse-text="<%= t('collavre.contacts.org_chart.collapse_all') %>">
12
+ <%= t('collavre.contacts.org_chart.expand_all') %>
13
+ </button>
14
+ </div>
15
+ <div class="org-chart-tree">
16
+ <% org_chart_roots.each do |creative| %>
17
+ <%= render partial: 'collavre/users/org_chart_node', locals: {
18
+ creative: creative,
19
+ org_chart_shares: org_chart_shares,
20
+ org_chart_invitations: org_chart_invitations,
21
+ org_chart_children: org_chart_children,
22
+ depth: 0
23
+ } %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
27
+
28
+ <% if org_chart_unassigned.present? %>
29
+ <div class="org-chart-unassigned">
30
+ <h3 class="org-chart-unassigned-title"><%= t('collavre.contacts.org_chart.unassigned_users') %></h3>
31
+ <div class="org-chart-members">
32
+ <% org_chart_unassigned.each do |user| %>
33
+ <div class="org-chart-member-row">
34
+ <div class="org-chart-member-info">
35
+ <%= render Collavre::AvatarComponent.new(
36
+ user: user,
37
+ size: 24,
38
+ classes: 'avatar'
39
+ ) %>
40
+ <div class="org-chart-member-name-group">
41
+ <%= link_to user.display_name, collavre.user_path(user), class: "org-chart-member-name" %>
42
+ <span class="org-chart-member-email"><%= user.email %></span>
43
+ </div>
44
+ </div>
45
+ <div class="org-chart-member-actions">
46
+ <%= link_to t('collavre.users.edit_ai.link'),
47
+ collavre.edit_ai_user_path(user),
48
+ class: "btn btn-xs btn-secondary" %>
49
+ <%= link_to t('collavre.users.copy_ai.link'),
50
+ collavre.new_ai_users_path(copy_from: user.id),
51
+ class: "btn btn-xs btn-secondary" %>
52
+ <%= button_to t('collavre.users.destroy.delete_ai_user'),
53
+ collavre.user_path(user),
54
+ method: :delete,
55
+ form: { data: { turbo_confirm: t('collavre.users.destroy.confirm_ai') } },
56
+ class: "btn btn-xs btn-danger" %>
57
+ </div>
58
+ </div>
59
+ <% end %>
60
+ </div>
61
+ </div>
62
+ <% end %>
63
+
64
+ <% if org_chart_roots.blank? && org_chart_unassigned.blank? %>
65
+ <p class="text-muted"><%= t('collavre.contacts.org_chart.empty_state') %></p>
66
+ <% end %>
67
+
68
+ </div>
@@ -0,0 +1,169 @@
1
+ <% effective = creative.origin_id? ? creative.effective_origin : creative %>
2
+ <% shares = org_chart_shares.fetch(effective.id, []) %>
3
+ <% invitations = org_chart_invitations.fetch(effective.id, []) %>
4
+ <% children = org_chart_children.fetch(creative.id, []) %>
5
+ <% has_content = shares.any? || invitations.any? || children.any? %>
6
+ <% is_admin = effective.user_id == Current.user.id || shares.any? { |s| s.user_id == Current.user.id && s.permission == "admin" } %>
7
+
8
+ <details class="org-chart-group" <%= 'open' if depth == 0 %>>
9
+ <summary class="org-chart-creative-row <%= 'org-chart-depth-0' if depth == 0 %>" style="--depth: <%= depth %>">
10
+ <span class="org-chart-creative-icon"><%= depth == 0 ? '📂' : '📁' %></span>
11
+ <%= link_to strip_tags(creative.effective_description.to_s).truncate(60),
12
+ collavre.creative_path(creative),
13
+ class: "org-chart-creative-link" %>
14
+ <% if effective.user_id == Current.user.id %>
15
+ <span class="org-chart-owner-badge"><%= t('collavre.contacts.org_chart.owner') %></span>
16
+ <% else %>
17
+ <% my_share = shares.find { |s| s.user_id == Current.user.id } %>
18
+ <% if my_share&.shared_by %>
19
+ <span class="org-chart-direction org-chart-direction-incoming">
20
+ <%= t('collavre.contacts.org_chart.shared_by_user', user: my_share.shared_by.display_name) %>
21
+ </span>
22
+ <% end %>
23
+ <% end %>
24
+ <% if is_admin %>
25
+ <button type="button"
26
+ class="btn btn-xs btn-secondary org-chart-manage-btn"
27
+ data-share-modal-url-param="<%= collavre.creative_creative_shares_path(effective) %>"
28
+ data-action="click->share-modal#open">
29
+ <%= t('collavre.contacts.org_chart.manage_permissions') %>
30
+ </button>
31
+ <%= link_to t('collavre.contacts.org_chart.add_ai_agent'),
32
+ collavre.new_ai_users_path(creative_id: effective.id),
33
+ class: "btn btn-xs btn-secondary org-chart-manage-btn" %>
34
+ <% end %>
35
+ </summary>
36
+
37
+ <div class="org-chart-content">
38
+ <% if shares.any? %>
39
+ <div class="org-chart-members" style="--indent: <%= depth %>">
40
+ <% shares.sort_by { |s| Collavre::CreativeShare.permissions[s.permission] }.reverse.each do |share| %>
41
+ <% next unless share.user %>
42
+ <div class="org-chart-member-row">
43
+ <%# Column 1: User info %>
44
+ <div class="org-chart-member-info">
45
+ <%= render Collavre::AvatarComponent.new(
46
+ user: share.user,
47
+ size: 24,
48
+ classes: "avatar org-chart-avatar"
49
+ ) %>
50
+ <div class="org-chart-member-name-group">
51
+ <%= link_to share.user.display_name, collavre.user_path(share.user), class: "org-chart-member-name" %>
52
+ <span class="org-chart-member-email"><%= share.user.email %></span>
53
+ </div>
54
+ </div>
55
+
56
+ <%# Column 2: Permission %>
57
+ <div class="org-chart-member-permission">
58
+ <% if is_admin %>
59
+ <select class="org-chart-permission-select org-chart-permission-<%= share.permission %>"
60
+ data-share-id="<%= share.id %>"
61
+ data-update-url="<%= collavre.creative_creative_share_path(effective, share) %>"
62
+ data-action="change->org-chart#updatePermission">
63
+ <% %w[admin write feedback read no_access].each do |perm| %>
64
+ <option value="<%= perm %>" <%= 'selected' if share.permission == perm %>>
65
+ <%= t("collavre.contacts.org_chart.permissions.#{perm}") %>
66
+ </option>
67
+ <% end %>
68
+ </select>
69
+ <% else %>
70
+ <span class="org-chart-permission-badge org-chart-permission-<%= share.permission %>">
71
+ <%= t("collavre.contacts.org_chart.permissions.#{share.permission}") %>
72
+ </span>
73
+ <% end %>
74
+ </div>
75
+
76
+ <%# Column 3: Actions %>
77
+ <div class="org-chart-member-actions">
78
+ <% if share.user.ai_user? && share.user.created_by_id == Current.user.id %>
79
+ <%= link_to t('collavre.users.edit_ai.link'), collavre.edit_ai_user_path(share.user), class: "btn btn-xs btn-secondary" %>
80
+ <%= link_to t('collavre.users.copy_ai.link'), collavre.new_ai_users_path(copy_from: share.user.id), class: "btn btn-xs btn-secondary" %>
81
+ <%= button_to t('collavre.users.destroy.delete_ai_user'),
82
+ collavre.user_path(share.user),
83
+ method: :delete,
84
+ form: { data: { turbo_confirm: t('collavre.users.destroy.confirm_ai') } },
85
+ class: "btn btn-xs btn-danger" %>
86
+ <% end %>
87
+ <%= link_to t('collavre.contacts.org_chart.view_profile'), collavre.user_path(share.user), class: "btn btn-xs btn-secondary" %>
88
+ <% if is_admin %>
89
+ <%= button_to t('collavre.contacts.org_chart.unshare'),
90
+ collavre.creative_creative_share_path(effective, share),
91
+ method: :delete,
92
+ form: { data: { turbo_confirm: t('collavre.contacts.org_chart.unshare_confirm', user: share.user.display_name) } },
93
+ class: "btn btn-xs btn-danger" %>
94
+ <% end %>
95
+ </div>
96
+ </div>
97
+ <% end %>
98
+ </div>
99
+ <% end %>
100
+
101
+ <% if invitations.any? && is_admin %>
102
+ <div class="org-chart-members" style="--indent: <%= depth %>">
103
+ <% invitations.each do |invitation| %>
104
+ <div class="org-chart-member-row org-chart-invitation-row">
105
+ <%# Column 1: Invitation info %>
106
+ <div class="org-chart-member-info">
107
+ <div class="avatar-wrapper" style="width: 24px; height: 24px;">
108
+ <% if invitation.email.present? %>
109
+ <%= image_tag asset_path("default_avatar.svg"),
110
+ alt: '', width: 24, height: 24,
111
+ class: 'avatar',
112
+ title: invitation.email,
113
+ style: 'border-radius:50%;vertical-align:middle;' %>
114
+ <span class="avatar-initial" style="font-size: 12px;"><%= invitation.email[0]&.upcase %></span>
115
+ <% else %>
116
+ <span class="avatar" style="display:inline-block;width:24px;height:24px;border-radius:50%;background:var(--surface-3,#ddd);text-align:center;line-height:24px;font-size:12px;">🔗</span>
117
+ <% end %>
118
+ </div>
119
+ <div class="org-chart-member-name-group">
120
+ <span class="org-chart-member-email"><%= invitation.email.presence || t('collavre.creatives.share.public_invite') %></span>
121
+ <span class="org-chart-pending-badge"><%= t('collavre.contacts.org_chart.pending_status') %></span>
122
+ </div>
123
+ </div>
124
+
125
+ <%# Column 2: Permission %>
126
+ <div class="org-chart-member-permission">
127
+ <select class="org-chart-permission-select org-chart-permission-<%= invitation.permission %>"
128
+ data-update-url="<%= collavre.creative_invitation_path(effective, invitation) %>"
129
+ data-action="change->org-chart#updatePermission">
130
+ <% %w[admin write feedback read no_access].each do |perm| %>
131
+ <option value="<%= perm %>" <%= 'selected' if invitation.permission == perm %>>
132
+ <%= t("collavre.contacts.org_chart.permissions.#{perm}") %>
133
+ </option>
134
+ <% end %>
135
+ </select>
136
+ </div>
137
+
138
+ <%# Column 3: Actions %>
139
+ <div class="org-chart-member-actions">
140
+ <%= button_to t('collavre.contacts.org_chart.cancel_invite'),
141
+ collavre.creative_invitation_path(effective, invitation),
142
+ method: :delete,
143
+ form: { data: { turbo_confirm: t('collavre.contacts.org_chart.cancel_invite_confirm', email: invitation.email.presence || t('collavre.creatives.share.public_invite')) } },
144
+ class: "btn btn-xs btn-danger" %>
145
+ </div>
146
+ </div>
147
+ <% end %>
148
+ </div>
149
+ <% end %>
150
+
151
+ <% if children.any? %>
152
+ <div class="org-chart-children" style="--indent: <%= depth %>">
153
+ <% children.each do |child| %>
154
+ <%= render partial: 'collavre/users/org_chart_node', locals: {
155
+ creative: child,
156
+ org_chart_shares: org_chart_shares,
157
+ org_chart_invitations: org_chart_invitations,
158
+ org_chart_children: org_chart_children,
159
+ depth: depth + 1
160
+ } %>
161
+ <% end %>
162
+ </div>
163
+ <% end %>
164
+
165
+ <% unless has_content %>
166
+ <p class="org-chart-no-members" style="--indent: <%= depth %>"><%= t('collavre.contacts.org_chart.no_members') %></p>
167
+ <% end %>
168
+ </div>
169
+ </details>
@@ -2,6 +2,15 @@
2
2
  <h1 class="text-center" style="margin-top: 0;"><%= t('collavre.users.new_ai.title') %></h1>
3
3
 
4
4
  <%= form_with url: collavre.create_ai_users_path, class: "stacked-form" do |form| %>
5
+ <% if params[:creative_id].present? %>
6
+ <%= form.hidden_field :creative_id, value: params[:creative_id] %>
7
+ <% creative = Collavre::Creative.find_by(id: params[:creative_id]) %>
8
+ <% if creative %>
9
+ <div class="form-group">
10
+ <p class="text-muted"><%= t('collavre.contacts.org_chart.add_ai_agent_to', creative: creative.plain_description) %></p>
11
+ </div>
12
+ <% end %>
13
+ <% end %>
5
14
  <div class="form-group">
6
15
  <%= form.label :ai_id, t('collavre.users.new_ai.id_label') %>
7
16
  <%= form.text_field :ai_id, required: true, class: "stacked-form-control", placeholder: "e.g. my_bot", value: @copy_source ? "#{@copy_source.email.split('@').first}_copy" : nil %>
@@ -130,14 +130,38 @@
130
130
  </section>
131
131
 
132
132
  <section class="tab-panel <%= 'active' if active_tab == 'contacts' %>" data-tabs-target="panel" data-tab-name="contacts">
133
- <%= render partial: 'collavre/users/contact_management', locals: {
134
- contacts: @contacts,
135
- last_login_map: @last_login_map,
136
- shared_by_me: @shared_by_me,
137
- shared_with_me: @shared_with_me,
138
- contact_page: @contact_page,
139
- total_contact_pages: @total_contact_pages
140
- } %>
133
+ <div class="flex-row-center-between">
134
+ <div class="contacts-view-switcher">
135
+ <%= link_to t('collavre.contacts.views.list'),
136
+ collavre.user_path(Current.user, tab: 'contacts', contacts_view: 'list'),
137
+ class: "btn btn-xs #{@contacts_view == 'list' ? 'btn-primary' : 'btn-secondary'}" %>
138
+ <%= link_to t('collavre.contacts.views.org_chart'),
139
+ collavre.user_path(Current.user, tab: 'contacts', contacts_view: 'org_chart'),
140
+ class: "btn btn-xs #{@contacts_view == 'org_chart' ? 'btn-primary' : 'btn-secondary'}" %>
141
+ </div>
142
+ <div>
143
+ <p class="text-muted contacts-description"><%= t('collavre.contacts.description') %></p>
144
+ <%= link_to t('collavre.users.add_ai_user'), collavre.new_ai_users_path, class: "primary-action-button" %>
145
+ </div>
146
+ </div>
147
+
148
+ <% if @contacts_view == 'org_chart' %>
149
+ <%= render partial: 'collavre/users/org_chart', locals: {
150
+ org_chart_roots: @org_chart_roots,
151
+ org_chart_shares: @org_chart_shares,
152
+ org_chart_invitations: @org_chart_invitations,
153
+ org_chart_children: @org_chart_children,
154
+ org_chart_unassigned: @org_chart_unassigned
155
+ } %>
156
+ <% else %>
157
+ <%= render partial: 'collavre/users/contact_list', locals: {
158
+ contacts: @contacts,
159
+ shared_by_me: @shared_by_me,
160
+ shared_with_me: @shared_with_me,
161
+ contact_page: @contact_page,
162
+ total_contact_pages: @total_contact_pages
163
+ } %>
164
+ <% end %>
141
165
  </section>
142
166
  </div>
143
167
  </div>
@@ -5,6 +5,9 @@ en:
5
5
  delete_confirm: This will delete all messages in this topic. Are you sure?
6
6
  new_placeholder: New Topic
7
7
  no_permission: You don't have permission to perform this action.
8
+ move:
9
+ no_target_permission: You don't have write permission on the target creative.
10
+ duplicate_name: A topic named '%{name}' already exists in the target creative.
8
11
  github_auth:
9
12
  login_first: Please sign in first.
10
13
  connected: Github account connected successfully.
@@ -66,12 +69,29 @@ en:
66
69
  speech_unavailable: Speech recognition is not supported in this browser.
67
70
  move_button: Move
68
71
  review_button: Review
72
+ review_select_hint: Please select text to review
73
+ review_feedback_placeholder: Write feedback for this quote...
74
+ review_summary_placeholder: Overall comment (optional)...
75
+ review_add_quote: "+ Add"
76
+ review_send: Send review
77
+ review_send_question: Send question
78
+ review_type_review: Review
79
+ review_type_question: Question
69
80
  replace_button: Replace
70
81
  move_no_selection: Select at least one message to move.
71
82
  move_error: Unable to move messages.
72
83
  add_participant: Add user
73
- hint_drag_topic: "🎯 Drag → Move to topic"
74
- hint_move_button: "📤 Move button → Another chat"
84
+ selection_count: "{count} selected"
85
+ selection_delete: Delete
86
+ selection_move: Move
87
+ selection_topic_move: Move to topic
88
+ selection_close: Cancel
89
+ selection_drag_hint: "You can also drag & drop to move to a topic"
90
+ batch_delete_confirm: Are you sure you want to delete the selected messages?
91
+ batch_delete_no_selection: Select at least one message to delete.
92
+ batch_delete_not_found: Some selected messages could not be found.
93
+ topic_search_placeholder: Search topics...
94
+ topic_main: Main
75
95
  fullscreen: Full screen
76
96
  exit_fullscreen: Exit full screen
77
97
  move_invalid_target: Select a valid creative to move messages to.
@@ -85,18 +105,53 @@ en:
85
105
  calendar_args: "YYYY-MM-DD@HH:MM memo (today/tomorrow, +3days, +2weeks, +1months, +1years, +mon)"
86
106
  topic_description: "Create a new topic with optional primary agent."
87
107
  topic_args: '"topic name" @agent_name'
108
+ work_description: "Create a workflow to execute tasks on child creatives via DFS traversal."
109
+ work_args: "@agent_name workflow_context"
110
+ compress_description: "Summarize all messages in the current topic and remove originals."
111
+ compress_args: "[additional instructions]"
112
+ creative_description: "Insert a link to a creative. In AI chats, the linked creative tree is injected as context."
88
113
  calendar_command:
89
114
  event_created: 'event created: [Google Calendar event](%{url})'
90
115
  event_created_local: 'event created (local only - connect Google Calendar to sync)'
91
116
  event_created_sync_failed: 'event created (Google Calendar sync failed - please reconnect your Google account)'
92
117
  mcp_command:
93
118
  error_running: "Error running /%{tool_name}: %{error}"
119
+ compress_command:
120
+ not_authorized: "You need write permission to compress a topic."
121
+ topic_required: "The /compress command can only be used within a topic."
122
+ nothing_to_compress: "No messages to compress in this topic."
123
+ started: "⏳ Compressing topic messages..."
124
+ summary_title: "Topic Summary — %{topic}"
125
+ failed: "Compress failed"
94
126
  topic_command:
95
127
  missing_name: 'Please specify a topic name in quotes: /topic "topic name"'
96
128
  created: 'Topic "%{name}" created.'
97
129
  created_with_agent: 'Topic "%{name}" created with @%{agent} as primary agent.'
98
130
  updated_agent: 'Topic "%{name}" primary agent updated to @%{agent}.'
99
131
  already_exists: 'Topic "%{name}" already exists.'
132
+ work_command:
133
+ agent_not_found: "Please mention an AI agent to execute the workflow."
134
+ no_children: "This creative has no child creatives to work on."
135
+ all_already_tasked: "All child creatives already have active tasks."
136
+ started: 'Workflow started with @%{agent}: %{total} tasks queued (%{skipped} skipped).'
137
+ trigger_comment: '📋 [Workflow] %{context}'
138
+ subtask_started: '📋 @%{agent} starting work on "%{creative}" (%{current}/%{total})'
139
+ subtask_completed: '✅ @%{agent} completed "%{creative}" (%{completed}/%{total}, %{progress}%%)'
140
+ workflow_completed: '✅ Workflow completed by @%{agent}: %{completed} creatives processed.'
141
+ workflow_failed: 'Workflow failed by @%{agent} on "%{creative}": %{reason}'
142
+ workflow_stopped: '⏹️ Workflow stopped by @%{agent}. %{completed} completed, %{remaining} remaining.'
143
+ workflow_resumed: '▶️ Workflow resumed by @%{agent}. %{remaining} tasks remaining.'
144
+ stopped: 'Workflow stopped for @%{agent}.'
145
+ resumed: 'Workflow resumed for @%{agent}: %{remaining} tasks remaining.'
146
+ no_active_workflow: 'No active workflow found on this creative.'
147
+ no_resumable_workflow: 'No failed or stopped workflow found to resume.'
148
+ supervisor_instruction: "If you need clarification, have questions, or are unsure about anything, ask @%{supervisor}: instead of the user. They will guide you."
149
+ versions:
150
+ previous: Previous version
151
+ next: Next version
152
+ delete: Delete this version
153
+ delete_confirm: Are you sure you want to delete this version?
154
+ select: Select
100
155
  read_by: Read by %{name}
101
156
  activity_logs_summary: Activity Logs
102
157
  calendar_events:
@@ -5,6 +5,9 @@ ko:
5
5
  delete_confirm: 이 토픽의 모든 메시지가 삭제됩니다. 확실합니까?
6
6
  new_placeholder: 새 토픽
7
7
  no_permission: 이 작업을 수행할 권한이 없습니다.
8
+ move:
9
+ no_target_permission: 대상 크리에이티브에 대한 쓰기 권한이 없습니다.
10
+ duplicate_name: "'%{name}' 토픽이 대상 크리에이티브에 이미 존재합니다."
8
11
  github_auth:
9
12
  login_first: 먼저 로그인해주세요.
10
13
  connected: Github 계정이 연동되었습니다.
@@ -63,12 +66,29 @@ ko:
63
66
  speech_unavailable: 이 브라우저는 음성 인식을 지원하지 않습니다.
64
67
  move_button: 이동
65
68
  review_button: 리뷰
69
+ review_select_hint: 리뷰할 텍스트를 선택해 주세요
70
+ review_feedback_placeholder: 이 인용에 대한 피드백을 입력하세요...
71
+ review_summary_placeholder: 종합 코멘트 (선택)...
72
+ review_add_quote: "+ 추가"
73
+ review_send: 리뷰 전송
74
+ review_send_question: 질문 보내기
75
+ review_type_review: 리뷰
76
+ review_type_question: 질문
66
77
  replace_button: 바꾸기
67
78
  move_no_selection: 이동할 메시지를 선택해주세요.
68
79
  move_error: 메시지를 이동할 수 없습니다.
69
80
  add_participant: 사용자 추가
70
- hint_drag_topic: "🎯 드래그 → 토픽 이동"
71
- hint_move_button: "📤 이동 버튼 → 다른 채팅창"
81
+ selection_count: "{count}개 선택"
82
+ selection_delete: 삭제
83
+ selection_move: 이동
84
+ selection_topic_move: 토픽 이동
85
+ selection_close: 취소
86
+ selection_drag_hint: "드래그&드롭으로도 토픽 이동 가능"
87
+ batch_delete_confirm: 선택한 메시지를 삭제하시겠습니까?
88
+ batch_delete_no_selection: 삭제할 메시지를 선택해주세요.
89
+ batch_delete_not_found: 일부 선택한 메시지를 찾을 수 없습니다.
90
+ topic_search_placeholder: 토픽 검색...
91
+ topic_main: 메인
72
92
  fullscreen: 전체 화면
73
93
  exit_fullscreen: 전체 화면 종료
74
94
  move_invalid_target: 이동할 크리에이티브를 선택해주세요.
@@ -82,18 +102,53 @@ ko:
82
102
  calendar_args: "YYYY-MM-DD@HH:MM 메모 (today/tomorrow, +3days, +2weeks, +1months, +1years, +mon/+월)"
83
103
  topic_description: "새 토픽을 생성합니다. Primary Agent를 지정할 수 있습니다."
84
104
  topic_args: '"토픽 이름" @에이전트이름'
105
+ work_description: "하위 크리에이티브들에 대해 DFS 순회를 통한 워크플로우를 실행합니다."
106
+ work_args: "@에이전트이름 워크플로우_컨텍스트"
107
+ compress_description: "현재 토픽의 모든 메세지를 요약하고 원본을 삭제합니다."
108
+ compress_args: "[추가 지시사항]"
109
+ creative_description: "크리에이티브 링크를 삽입합니다. AI 채팅에서는 링크된 크리에이티브 트리가 컨텍스트로 주입됩니다."
85
110
  calendar_command:
86
111
  event_created: '이벤트가 생성되었습니다: [구글 캘린더 이벤트](%{url})'
87
112
  event_created_local: '이벤트가 생성되었습니다 (로컬 전용 - 구글 캘린더 연동 시 동기화됩니다)'
88
113
  event_created_sync_failed: '이벤트가 생성되었습니다 (구글 캘린더 동기화 실패 - 구글 계정을 다시 연동해주세요)'
89
114
  mcp_command:
90
115
  error_running: "/%{tool_name} 실행 오류: %{error}"
116
+ compress_command:
117
+ not_authorized: "토픽을 요약하려면 쓰기 권한이 필요합니다."
118
+ topic_required: "/compress 명령은 토픽 내에서만 사용할 수 있습니다."
119
+ nothing_to_compress: "이 토픽에 요약할 메세지가 없습니다."
120
+ started: "⏳ 토픽 메세지를 요약하는 중..."
121
+ summary_title: "토픽 요약 — %{topic}"
122
+ failed: "요약 실패"
91
123
  topic_command:
92
124
  missing_name: '토픽 이름을 따옴표로 지정하세요: /topic "토픽 이름"'
93
125
  created: '토픽 "%{name}"이(가) 생성되었습니다.'
94
126
  created_with_agent: '토픽 "%{name}"이(가) 생성되었습니다. @%{agent}이(가) Primary Agent로 설정되었습니다.'
95
127
  updated_agent: '토픽 "%{name}"의 Primary Agent가 @%{agent}(으)로 변경되었습니다.'
96
128
  already_exists: '토픽 "%{name}"이(가) 이미 존재합니다.'
129
+ work_command:
130
+ agent_not_found: "워크플로우를 실행할 AI 에이전트를 멘션해주세요."
131
+ no_children: "이 크리에이티브에는 작업할 하위 크리에이티브가 없습니다."
132
+ all_already_tasked: "모든 하위 크리에이티브가 이미 활성 작업을 가지고 있습니다."
133
+ started: '워크플로우가 @%{agent}와 함께 시작되었습니다: %{total}개 작업 대기중 (%{skipped}개 생략).'
134
+ trigger_comment: '📋 [워크플로우] %{context}'
135
+ subtask_started: '📋 @%{agent}가 "%{creative}" 작업을 시작합니다 (%{current}/%{total})'
136
+ subtask_completed: '✅ @%{agent}가 "%{creative}" 완료 (%{completed}/%{total}, %{progress}%%)'
137
+ workflow_completed: '✅ @%{agent}가 워크플로우를 완료했습니다: %{completed}개 크리에이티브 처리됨.'
138
+ workflow_failed: '@%{agent} 워크플로우 실패 - "%{creative}": %{reason}'
139
+ workflow_stopped: '⏹️ @%{agent} 워크플로우가 중지되었습니다. %{completed}개 완료, %{remaining}개 남음.'
140
+ workflow_resumed: '▶️ @%{agent} 워크플로우가 재개되었습니다. %{remaining}개 작업 남음.'
141
+ stopped: '@%{agent}의 워크플로우가 중지되었습니다.'
142
+ resumed: '@%{agent}의 워크플로우가 재개되었습니다: %{remaining}개 작업 남음.'
143
+ no_active_workflow: '이 크리에이티브에 활성 워크플로우가 없습니다.'
144
+ no_resumable_workflow: '재개할 수 있는 실패/중지된 워크플로우가 없습니다.'
145
+ supervisor_instruction: "질문이 있거나 확인이 필요하면 사용자 대신 @%{supervisor}: 에게 물어보세요. 감독자가 안내해줄 것입니다."
146
+ versions:
147
+ previous: 이전 버전
148
+ next: 다음 버전
149
+ delete: 이 버전 삭제
150
+ delete_confirm: 이 버전을 삭제하시겠습니까?
151
+ select: 선택
97
152
  read_by: "%{name} 님이 읽음"
98
153
  activity_logs_summary: 활동 기록
99
154
  calendar_events: