collavre 0.20.2 → 0.21.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/actiontext.css +92 -2
  3. data/app/assets/stylesheets/collavre/code_highlight.css +144 -26
  4. data/app/assets/stylesheets/collavre/comments_popup.css +83 -0
  5. data/app/assets/stylesheets/collavre/landing.css +507 -0
  6. data/app/channels/collavre/comments_presence_channel.rb +7 -0
  7. data/app/controllers/collavre/admin/integrations_controller.rb +82 -0
  8. data/app/controllers/collavre/admin/settings_controller.rb +22 -17
  9. data/app/controllers/collavre/application_controller.rb +27 -0
  10. data/app/controllers/collavre/channels_controller.rb +23 -0
  11. data/app/controllers/collavre/creatives_controller.rb +50 -6
  12. data/app/controllers/collavre/landing_controller.rb +8 -0
  13. data/app/controllers/collavre/passwords_controller.rb +1 -0
  14. data/app/controllers/collavre/public_assets_controller.rb +24 -0
  15. data/app/controllers/collavre/topics_controller.rb +21 -30
  16. data/app/helpers/collavre/comments_helper.rb +7 -0
  17. data/app/helpers/collavre/public_assets_helper.rb +14 -0
  18. data/app/javascript/controllers/comment_controller.js +9 -0
  19. data/app/javascript/controllers/comments/form_controller.js +4 -0
  20. data/app/javascript/controllers/comments/list_controller.js +10 -7
  21. data/app/javascript/controllers/comments/popup_controller.js +9 -0
  22. data/app/javascript/controllers/comments/presence_controller.js +83 -1
  23. data/app/javascript/controllers/comments/topics_controller.js +15 -0
  24. data/app/javascript/controllers/creatives/__tests__/sync_controller.test.js +89 -0
  25. data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +120 -0
  26. data/app/javascript/controllers/creatives/sync_controller.js +30 -9
  27. data/app/javascript/controllers/creatives/tree_controller.js +23 -0
  28. data/app/javascript/controllers/index.js +4 -1
  29. data/app/javascript/controllers/landing_video_controller.js +53 -0
  30. data/app/javascript/creatives/tree_renderer.js +6 -0
  31. data/app/javascript/lib/api/__tests__/queue_manager.test.js +27 -0
  32. data/app/javascript/lib/api/queue_manager.js +17 -5
  33. data/app/javascript/modules/__tests__/command_args_form.test.js +103 -0
  34. data/app/javascript/modules/__tests__/html_content_empty.test.js +41 -0
  35. data/app/javascript/modules/__tests__/markdown_source_reconcile.test.js +70 -0
  36. data/app/javascript/modules/command_args_form.js +22 -4
  37. data/app/javascript/modules/command_menu.js +27 -0
  38. data/app/javascript/modules/creative_row_editor.js +227 -17
  39. data/app/javascript/modules/html_content_empty.js +12 -0
  40. data/app/javascript/modules/markdown_source_reconcile.js +53 -0
  41. data/app/jobs/collavre/drop_trigger_job.rb +37 -8
  42. data/app/mailers/collavre/application_mailer.rb +1 -1
  43. data/app/models/collavre/channel/injected_message.rb +5 -0
  44. data/app/models/collavre/channel.rb +87 -0
  45. data/app/models/collavre/creative/describable.rb +65 -3
  46. data/app/models/collavre/creative.rb +2 -0
  47. data/app/models/collavre/integration_setting.rb +35 -0
  48. data/app/models/collavre/preview_channel.rb +93 -0
  49. data/app/models/collavre/system_setting.rb +13 -2
  50. data/app/models/collavre/topic.rb +3 -25
  51. data/app/models/concerns/collavre/ai_agent_resolvable.rb +12 -3
  52. data/app/services/collavre/ai_client.rb +3 -3
  53. data/app/services/collavre/channel_attacher.rb +58 -0
  54. data/app/services/collavre/comments/mcp_command.rb +31 -1
  55. data/app/services/collavre/creatives/tree_builder.rb +7 -3
  56. data/app/services/collavre/google_calendar_service.rb +4 -2
  57. data/app/services/collavre/markdown_converter.rb +130 -15
  58. data/app/services/collavre/markdown_importer.rb +7 -2
  59. data/app/services/collavre/orchestration/policy_resolver.rb +11 -1
  60. data/app/services/collavre/tools/creative_attach_files_service.rb +96 -0
  61. data/app/services/collavre/tools/creative_list_attachments_service.rb +42 -0
  62. data/app/services/collavre/tools/creative_remove_attachment_service.rb +35 -0
  63. data/app/services/collavre/tools/permission_denied_error.rb +9 -0
  64. data/app/services/collavre/tools/preview_attach_service.rb +128 -0
  65. data/app/services/collavre/tools/preview_detach_service.rb +61 -0
  66. data/app/services/collavre/tools/topic_authorizer.rb +24 -0
  67. data/app/services/collavre/topic_branch_service.rb +34 -26
  68. data/app/views/admin/shared/_tabs.html.erb +1 -0
  69. data/app/views/collavre/admin/integrations/_category.html.erb +22 -0
  70. data/app/views/collavre/admin/integrations/_setting_row.html.erb +54 -0
  71. data/app/views/collavre/admin/integrations/index.html.erb +42 -0
  72. data/app/views/collavre/admin/settings/_system_tab.html.erb +8 -0
  73. data/app/views/collavre/comments/_channel_chips.html.erb +33 -0
  74. data/app/views/collavre/comments/_comment.html.erb +6 -1
  75. data/app/views/collavre/comments/_comments_popup.html.erb +1 -0
  76. data/app/views/collavre/creatives/_inline_edit_form.html.erb +19 -0
  77. data/app/views/collavre/creatives/index.html.erb +10 -2
  78. data/app/views/collavre/landing/show.html.erb +130 -0
  79. data/app/views/layouts/collavre/landing.html.erb +33 -0
  80. data/config/locales/admin.en.yml +4 -2
  81. data/config/locales/admin.ko.yml +4 -2
  82. data/config/locales/channels.en.yml +11 -0
  83. data/config/locales/channels.ko.yml +11 -0
  84. data/config/locales/comments.en.yml +2 -0
  85. data/config/locales/comments.ko.yml +2 -0
  86. data/config/locales/creatives.en.yml +9 -0
  87. data/config/locales/creatives.ko.yml +8 -0
  88. data/config/locales/integrations.en.yml +44 -0
  89. data/config/locales/integrations.ko.yml +44 -0
  90. data/config/locales/landing.en.yml +51 -0
  91. data/config/locales/landing.ko.yml +51 -0
  92. data/config/routes.rb +18 -0
  93. data/db/migrate/20260526000000_create_channels.rb +42 -0
  94. data/db/migrate/20260527000000_add_dismissed_at_to_channels.rb +6 -0
  95. data/db/migrate/20260527000100_backfill_dismissed_at_for_legacy_detached_channels.rb +28 -0
  96. data/db/migrate/20260528000000_add_preview_channel_unique_index.rb +31 -0
  97. data/db/migrate/20260529000000_add_primary_agent_id_to_topics.rb +40 -0
  98. data/db/migrate/20260529100000_create_integration_settings.rb +15 -0
  99. data/db/seeds.rb +19 -0
  100. data/lib/collavre/aws_credentials.rb +75 -0
  101. data/lib/collavre/engine.rb +51 -0
  102. data/lib/collavre/integration_settings/key_definition.rb +29 -0
  103. data/lib/collavre/integration_settings/registry.rb +55 -0
  104. data/lib/collavre/integration_settings/resolver.rb +71 -0
  105. data/lib/collavre/integration_settings.rb +46 -0
  106. data/lib/collavre/ses_settings_interceptor.rb +72 -0
  107. data/lib/collavre/version.rb +1 -1
  108. data/lib/collavre.rb +3 -0
  109. metadata +52 -1
@@ -4,16 +4,23 @@ module Collavre
4
4
 
5
5
  MAX_BRANCH_COMMENTS = 100
6
6
 
7
- def initialize(creative:, user:, source_topic:)
7
+ def initialize(creative:, user:, source_topic:, name: nil)
8
8
  @creative = creative
9
9
  @user = user
10
10
  @source_topic = source_topic
11
+ @name = name
11
12
  end
12
13
 
13
14
  # Creates a new topic with copies of the selected comments.
14
15
  # Returns the new Topic.
15
- def call(comment_ids:)
16
- comment_ids = Array(comment_ids).map(&:presence).compact.map(&:to_i).first(MAX_BRANCH_COMMENTS)
16
+ # enforce_limit: false bypasses MAX_BRANCH_COMMENTS for system-initiated
17
+ # full-history copies (e.g. Drop Trigger) where the UI's selection cap
18
+ # does not apply.
19
+ # auto_select: false omits user_id from the topic-created broadcast so
20
+ # background/system branches do not hijack the owner's current selection.
21
+ def call(comment_ids:, enforce_limit: true, auto_select: true)
22
+ comment_ids = Array(comment_ids).map(&:presence).compact.map(&:to_i)
23
+ comment_ids = comment_ids.first(MAX_BRANCH_COMMENTS) if enforce_limit
17
24
  raise BranchError, I18n.t("collavre.comments.branch.no_selection") if comment_ids.empty?
18
25
 
19
26
  validate_permissions!
@@ -24,14 +31,14 @@ module Collavre
24
31
  copy_comments(originals)
25
32
  end
26
33
 
27
- broadcast_topic_created
34
+ broadcast_topic_created(auto_select: auto_select)
28
35
 
29
36
  @new_topic
30
37
  end
31
38
 
32
39
  private
33
40
 
34
- attr_reader :creative, :user, :source_topic
41
+ attr_reader :creative, :user, :source_topic, :name
35
42
 
36
43
  def validate_permissions!
37
44
  unless creative.has_permission?(user, :feedback)
@@ -49,25 +56,28 @@ module Collavre
49
56
  end
50
57
 
51
58
  def create_branch_topic
52
- prefix = I18n.t("collavre.topics.branch_prefix")
53
- source_name = source_topic&.name || I18n.t("collavre.comments.topic_main", default: "All Messages")
54
- name = "#{prefix}:#{source_name}"
55
-
56
- # Ensure uniqueness
57
- existing = creative.topics.where("name LIKE ?", "#{Topic.sanitize_sql_like(name)}%").pluck(:name)
58
- if existing.include?(name)
59
- counter = 2
60
- counter += 1 while existing.include?("#{name} #{counter}")
61
- name = "#{name} #{counter}"
62
- end
59
+ topic_name = name.presence || default_branch_name
63
60
 
64
61
  creative.topics.create!(
65
- name: name,
62
+ name: topic_name,
66
63
  user: user,
67
64
  source_topic_id: source_topic&.id
68
65
  )
69
66
  end
70
67
 
68
+ def default_branch_name
69
+ prefix = I18n.t("collavre.topics.branch_prefix")
70
+ source_name = source_topic&.name || I18n.t("collavre.comments.topic_main", default: "All Messages")
71
+ candidate = "#{prefix}:#{source_name}"
72
+
73
+ existing = creative.topics.where("name LIKE ?", "#{Topic.sanitize_sql_like(candidate)}%").pluck(:name)
74
+ return candidate unless existing.include?(candidate)
75
+
76
+ counter = 2
77
+ counter += 1 while existing.include?("#{candidate} #{counter}")
78
+ "#{candidate} #{counter}"
79
+ end
80
+
71
81
  def copy_comments(originals)
72
82
  id_mapping = {}
73
83
 
@@ -98,15 +108,13 @@ module Collavre
98
108
  end
99
109
  end
100
110
 
101
- def broadcast_topic_created
102
- TopicsChannel.broadcast_to(
103
- creative,
104
- {
105
- action: "created",
106
- topic: { id: @new_topic.id, name: @new_topic.name, source_topic_id: @new_topic.source_topic_id },
107
- user_id: user.id
108
- }
109
- )
111
+ def broadcast_topic_created(auto_select: true)
112
+ payload = {
113
+ action: "created",
114
+ topic: { id: @new_topic.id, name: @new_topic.name, source_topic_id: @new_topic.source_topic_id }
115
+ }
116
+ payload[:user_id] = user.id if auto_select
117
+ TopicsChannel.broadcast_to(creative, payload)
110
118
  end
111
119
  end
112
120
  end
@@ -3,4 +3,5 @@
3
3
  <%= link_to t('admin.tabs.uiux'), collavre.admin_uiux_path, class: "tab-button #{'active' if controller_name == 'settings' && action_name == 'uiux'}" %>
4
4
  <%= link_to t('admin.tabs.users'), collavre.users_path, class: "tab-button #{'active' if controller_name == 'users'}" %>
5
5
  <%= link_to t('admin.tabs.orchestration'), collavre.admin_orchestration_path, class: "tab-button #{'active' if controller_name == 'orchestration'}" %>
6
+ <%= link_to t('collavre.admin.integrations.tab_label'), collavre.admin_integrations_path, class: "tab-button #{'active' if controller_name == 'integrations'}" %>
6
7
  </div>
@@ -0,0 +1,22 @@
1
+ <div style="margin-top: 2em; border-top: 1px solid var(--color-border); padding-top: 1.5em;">
2
+ <h3 style="font-size: 1.1em; margin-bottom: 1em;">
3
+ <%= t("collavre.admin.integrations.category.#{category}", default: category.to_s.humanize) %>
4
+ </h3>
5
+
6
+ <div class="table-scroll">
7
+ <table class="settings-table" style="width: 100%; min-width: 640px; border-collapse: collapse;">
8
+ <thead>
9
+ <tr>
10
+ <th style="text-align: left; padding: 0.5em; width: 30%;"><%= t("collavre.admin.integrations.headers.key") %></th>
11
+ <th style="text-align: left; padding: 0.5em; width: 50%;"><%= t("collavre.admin.integrations.headers.value") %></th>
12
+ <th style="text-align: left; padding: 0.5em; width: 20%;"><%= t("collavre.admin.integrations.headers.actions") %></th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% rows.each do |row| %>
17
+ <%= render partial: "setting_row", locals: { row: row, bulk_form_id: bulk_form_id } %>
18
+ <% end %>
19
+ </tbody>
20
+ </table>
21
+ </div>
22
+ </div>
@@ -0,0 +1,54 @@
1
+ <% definition = row[:definition] %>
2
+ <% key_str = definition.key.to_s %>
3
+ <%
4
+ source_bg, source_fg = case row[:source]
5
+ when :db then ["var(--color-success)", "var(--text-on-badge)"]
6
+ when :env then ["var(--color-warning)", "var(--text-on-badge)"]
7
+ else ["var(--surface-btn)", "var(--text-on-btn)"]
8
+ end
9
+ current_display = row[:present] ? row[:display_value].to_s : "—"
10
+ %>
11
+ <tr style="border-top: 1px solid var(--color-border);">
12
+ <td style="padding: 0.5em; vertical-align: top;">
13
+ <code><%= key_str %></code>
14
+ <% if definition.requires_restart %>
15
+ <span class="badge badge-warning" style="display: inline-block; margin-left: 0.5em; padding: 0.1em 0.5em; font-size: 0.75em; background: var(--color-warning); color: var(--text-on-badge); border-radius: 3px;">
16
+ <%= t("collavre.admin.integrations.restart_required") %>
17
+ </span>
18
+ <% end %>
19
+ <div style="font-size: 0.8em; color: var(--color-muted); margin-top: 0.25em;">
20
+ <%= t("collavre.admin.integrations.env_var_label") %>: <code><%= definition.env_var %></code>
21
+ </div>
22
+ </td>
23
+ <td style="padding: 0.5em; vertical-align: top;">
24
+ <%# Input references the bulk form via HTML5 `form` attribute so the table is not nested inside <form>. %>
25
+ <%# Placeholder displays the current value (masked for sensitive); empty input keeps the existing value. %>
26
+ <% input_tag = definition.sensitive ? :password_field_tag : :text_field_tag %>
27
+ <%= send(input_tag,
28
+ "integration_setting[#{key_str}]",
29
+ nil,
30
+ form: bulk_form_id,
31
+ placeholder: current_display,
32
+ autocomplete: "off",
33
+ style: "width: 100%;") %>
34
+ <div style="font-size: 0.8em; margin-top: 0.25em;">
35
+ <span class="badge badge-source-<%= row[:source] %>" style="display: inline-block; padding: 0.1em 0.5em; font-size: 0.85em; border-radius: 3px; background: <%= source_bg %>; color: <%= source_fg %>;">
36
+ <%= t("collavre.admin.integrations.source.#{row[:source]}", default: row[:source].to_s.upcase) %>
37
+ </span>
38
+ <% if definition.sensitive %>
39
+ <span style="margin-left: 0.5em; color: var(--color-muted);"><%= t("collavre.admin.integrations.sensitive") %></span>
40
+ <% end %>
41
+ <span style="margin-left: 0.5em; color: var(--color-muted);"><%= t("collavre.admin.integrations.placeholder_leave_blank") %></span>
42
+ </div>
43
+ </td>
44
+ <td style="padding: 0.5em; vertical-align: top; white-space: nowrap;">
45
+ <%= button_to t("collavre.admin.integrations.actions.reset"),
46
+ collavre.reset_admin_integration_path(key: key_str),
47
+ method: :delete,
48
+ form: {
49
+ style: "display: inline-block;",
50
+ data: { turbo_confirm: t("collavre.admin.integrations.confirm_reset") }
51
+ },
52
+ class: "btn btn-small btn-secondary" %>
53
+ </td>
54
+ </tr>
@@ -0,0 +1,42 @@
1
+ <h1 class="no-top-margin"><%= t("collavre.admin.integrations.title") %></h1>
2
+
3
+ <%= render "admin/shared/tabs" %>
4
+
5
+ <div class="tab-panels">
6
+ <section class="tab-panel active">
7
+ <%= tag.div(flash[:alert], class: "flash-alert") if flash[:alert] %>
8
+ <%= tag.div(flash[:warning], class: "flash-warning") if flash[:warning] %>
9
+ <%= tag.div(flash[:notice], class: "flash-notice") if flash[:notice] %>
10
+
11
+ <p style="color: var(--color-muted); margin-bottom: 1.5em;">
12
+ <%= t("collavre.admin.integrations.subtitle") %>
13
+ </p>
14
+
15
+ <% if @grouped_settings.empty? %>
16
+ <p class="empty-state" style="padding: 2em; text-align: center; color: var(--color-muted);">
17
+ <%= t("collavre.admin.integrations.empty_state") %>
18
+ </p>
19
+ <% else %>
20
+ <%# Bulk-save form is an empty container. Row inputs reference it via the HTML5
21
+ `form` attribute; per-row Reset/Seed `button_to` forms sit as siblings,
22
+ so we never nest <form> inside <form>. %>
23
+ <%= form_with url: collavre.bulk_update_admin_integrations_path,
24
+ method: :patch,
25
+ local: true,
26
+ id: "integrations-bulk-form",
27
+ html: { id: "integrations-bulk-form", class: "profile-form" } do %>
28
+ <% end %>
29
+
30
+ <% @grouped_settings.each do |category, rows| %>
31
+ <%= render partial: "category", locals: { category: category, rows: rows, bulk_form_id: "integrations-bulk-form" } %>
32
+ <% end %>
33
+
34
+ <div style="margin-top: 2em;">
35
+ <%= button_tag t("collavre.admin.integrations.actions.save"),
36
+ type: :submit,
37
+ form: "integrations-bulk-form",
38
+ class: "btn btn-primary" %>
39
+ </div>
40
+ <% end %>
41
+ </section>
42
+ </div>
@@ -38,6 +38,14 @@
38
38
  </div>
39
39
  </div>
40
40
 
41
+ <div style="margin-top: 1.5em;">
42
+ <%= f.label :home_page_path_authenticated, t('admin.settings.home_page_path_authenticated') %>
43
+ <%= f.text_field :home_page_path_authenticated, value: @home_page_path_authenticated, placeholder: "/creatives" %>
44
+ <div class="help-text" style="font-size: 0.85em; color: var(--color-text-muted); margin-top: 0.25em;">
45
+ <%= t('admin.settings.home_page_path_authenticated_hint') %>
46
+ </div>
47
+ </div>
48
+
41
49
  <div style="margin-top: 2em; border-top: 1px solid var(--color-border); padding-top: 1.5em;">
42
50
  <h3 style="font-size: 1.1em; margin-bottom: 1em;"><%= t('admin.settings.account_lockout') %></h3>
43
51
  <div style="display: flex; gap: 2em; flex-wrap: wrap;">
@@ -0,0 +1,33 @@
1
+ <%# locals: { topic: Collavre::Topic } %>
2
+ <div class="channel-chips" data-comments--presence-target="channelChips" data-topic-id="<%= topic.id %>">
3
+ <% topic.channels.not_dismissed.each do |channel| %>
4
+ <%# Chip is kept after detach so the final closed/stopped badge stays
5
+ visible until the user dismisses it with the X button. Subclasses
6
+ drive the colored badge via badge_state / badge_title; returning nil
7
+ from badge_state hides the badge entirely. %>
8
+ <% badge_state = channel.badge_state %>
9
+ <% badge_title = channel.badge_title %>
10
+ <% chip_classes = [ "channel-chip" ]
11
+ chip_classes << "channel-chip--detached" if channel.detached? %>
12
+ <% chip_label = channel.latest_label.presence || channel.default_label.presence || channel.class.name.demodulize %>
13
+ <% chip_link = channel.latest_link.presence || channel.default_link.presence %>
14
+ <span class="<%= chip_classes.join(' ') %>" data-channel-id="<%= channel.id %>">
15
+ <% if badge_state %>
16
+ <span class="channel-chip-badge channel-chip-badge--<%= badge_state %>"
17
+ title="<%= badge_title %>"
18
+ aria-label="<%= badge_title %>"></span>
19
+ <% end %>
20
+ <% if chip_link %>
21
+ <a href="<%= chip_link %>" target="_blank" rel="noopener">
22
+ <%= chip_label %>
23
+ </a>
24
+ <% else %>
25
+ <%= chip_label %>
26
+ <% end %>
27
+ <button type="button"
28
+ data-action="comments--presence#detachChannel"
29
+ data-channel-id="<%= channel.id %>"
30
+ aria-label="<%= t('collavre.channels.detach', default: 'Detach') %>">&times;</button>
31
+ </span>
32
+ <% end %>
33
+ </div>
@@ -164,7 +164,12 @@
164
164
  <details class="comment-action-details">
165
165
  <summary class="comment-action-summary"><%= t("collavre.comments.action_summary") %></summary>
166
166
  <div class="comment-action-body">
167
- <pre class="comment-action-json" data-comment-action-json><%= formatted_comment_action(comment) %></pre>
167
+ <% action_md = comment_action_markdown(comment) %>
168
+ <% if action_md && !has_pending_action %>
169
+ <div class="comment-content comment-action-markdown"><%= action_md %></div>
170
+ <% else %>
171
+ <pre class="comment-action-json" data-comment-action-json><%= formatted_comment_action(comment) %></pre>
172
+ <% end %>
168
173
  <% if has_pending_action %>
169
174
  <div class="comment-action-approve-controls comment-approve-hidden" data-comment-target="actionApproveControls">
170
175
  <button class="edit-comment-action-btn" type="button" data-comment-id="<%= comment.id %>"><%= t("collavre.comments.edit_action_button") %></button>
@@ -120,6 +120,7 @@
120
120
  <div id="comments-list" data-comments--popup-target="list" data-comments--list-target="list"><%= t('app.loading') %></div>
121
121
  <div id="typing-indicator-row">
122
122
  <button type="button" id="scroll-prev-msg-btn" class="scroll-prev-msg-btn" data-action="click->comments--list#scrollToPreviousMessage" title="<%= t('collavre.comments.scroll_to_prev', default: 'Previous message') %>" aria-label="<%= t('collavre.comments.scroll_to_prev', default: 'Previous message') %>">&#8963;</button>
123
+ <div id="channel-chips-container" data-comments--presence-target="channelChips"></div>
123
124
  <div id="typing-indicator" data-comments--presence-target="typingIndicator" data-comments--popup-target="typingIndicator" data-stop-agent-text="<%= t('collavre.comments.stop_agent') %>"></div>
124
125
  </div>
125
126
  <form id="new-comment-form" data-comments--popup-target="form" data-comments--form-target="form" style="display:none;">
@@ -3,6 +3,8 @@
3
3
  <input type="hidden" id="inline-method" name="_method" value="patch" />
4
4
  <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
5
5
  <input type="hidden" id="inline-creative-description" name="creative[description]" />
6
+ <input type="hidden" id="inline-content-type" name="creative[content_type_input]" value="html" />
7
+ <input type="hidden" id="inline-markdown-source" name="creative[markdown_source]" />
6
8
  <input type="hidden" id="inline-parent-id" name="creative[parent_id]" />
7
9
  <input type="hidden" id="inline-before-id" name="before_id" />
8
10
  <input type="hidden" id="inline-after-id" name="after_id" />
@@ -14,6 +16,14 @@
14
16
  data-direct-upload-url="<%= main_app.rails_direct_uploads_path %>"
15
17
  data-blob-url-template="<%= main_app.rails_service_blob_path(':signed_id', ':filename') %>"
16
18
  data-placeholder="<%= t('collavre.creatives.inline_editor.placeholder') %>"></div>
19
+ <div id="markdown-editor-wrapper" class="markdown-editor-wrapper" style="display:none;">
20
+ <textarea id="markdown-editor-textarea"
21
+ class="markdown-editor-textarea"
22
+ rows="10"
23
+ placeholder="<%= t('collavre.creatives.inline_editor.markdown_placeholder') %>"></textarea>
24
+ <div id="markdown-preview" class="markdown-preview"
25
+ data-placeholder="<%= t('collavre.creatives.inline_editor.markdown_preview_placeholder') %>"></div>
26
+ </div>
17
27
  <div id="actions-inline-editor" style="padding: 0px 8px;">
18
28
  <div style="margin-top:0.5em;display:flex;align-items:center;gap:0.5em;">
19
29
  <input type="hidden" name="creative[progress]" value="0">
@@ -30,6 +40,15 @@
30
40
  <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
41
  { }
32
42
  </button>
43
+ <button type="button" id="inline-toggle-markdown" class="creative-action-btn"
44
+ title="<%= t('collavre.creatives.index.toggle_markdown') %>"
45
+ data-label-markdown="<%= t('collavre.creatives.index.toggle_markdown') %>"
46
+ data-label-richtext="<%= t('collavre.creatives.index.toggle_richtext') %>"
47
+ data-confirm-to-richtext="<%= t('collavre.creatives.index.markdown_to_richtext_confirm') %>"
48
+ data-confirm-to-markdown="<%= t('collavre.creatives.index.richtext_to_markdown_confirm') %>"
49
+ style="margin-left:0.5em;font-family:monospace;font-size:0.9em;">
50
+ <%= t('collavre.creatives.index.toggle_markdown') %>
51
+ </button>
33
52
  </div>
34
53
  <div style="margin-top:0.5em;">
35
54
  <button type="button" id="inline-move-up" class="creative-action-btn" title="<%= t('collavre.creatives.index.inline_move_up_tooltip') %>">
@@ -154,17 +154,25 @@
154
154
  title_row_content += content_tag(:template, data: { part: "edit-icon" }) { svg_tag("edit.svg", class: "icon-edit") }
155
155
  title_row_content += content_tag(:template, data: { part: "edit-off-icon" }) { svg_tag("edit-off.svg", class: "icon-edit") }
156
156
  %>
157
+ <%
158
+ title_can_write = @parent_creative.has_permission?(Current.user, :write)
159
+ title_effective_origin = @parent_creative.effective_origin(Set.new)
160
+ title_content_type = title_effective_origin.data&.dig("content_type")
161
+ title_markdown_source = title_can_write ? title_effective_origin.data&.dig("markdown_source") : nil
162
+ %>
157
163
  <%= content_tag(
158
164
  "creative-tree-row",
159
165
  title_row_content,
160
166
  "dom-id": "creative-markdown-block",
161
167
  "creative-id": @parent_creative.id,
162
168
  "parent-id": @parent_creative.parent_id,
163
- "can-write": @parent_creative.has_permission?(Current.user, :write) ? "true" : "false",
169
+ "can-write": title_can_write ? "true" : "false",
164
170
  "is-title": "",
165
171
  "data-description-raw-html": @parent_creative.description,
166
172
  "data-progress-value": @parent_creative.progress,
167
- "data-origin-id": @parent_creative.origin_id
173
+ "data-origin-id": @parent_creative.origin_id,
174
+ "data-content-type": title_content_type,
175
+ "data-markdown-source": title_markdown_source
168
176
  ) %>
169
177
  <% else %>
170
178
  <%= content_tag(
@@ -0,0 +1,130 @@
1
+ <%# Landing Page — Collavre %>
2
+
3
+ <div class="landing">
4
+ <%# ═══════════════════════════════════════════ HERO ══════════════════════════════════════════ %>
5
+ <section class="landing-hero">
6
+ <div class="landing-hero-glow"></div>
7
+ <div class="landing-container">
8
+ <h1 class="landing-hero-title"><%= t('collavre.landing.hero.headline_html') %></h1>
9
+ <p class="landing-hero-sub"><%= t('collavre.landing.hero.subline') %></p>
10
+ <p class="landing-hero-brand"><%= t('app.name') %></p>
11
+ <div class="landing-hero-actions">
12
+ <a href="#problem" class="landing-btn landing-btn-lg"><%= t('collavre.landing.hero.learn_more') %></a>
13
+ <%= link_to t('app.sign_in'), collavre.new_session_path, class: "landing-btn landing-btn-ghost landing-btn-lg" %>
14
+ </div>
15
+ </div>
16
+ </section>
17
+
18
+ <%# ══════════════════════════════════════ PROBLEM STATEMENT ══════════════════════════════════ %>
19
+ <section id="problem" class="landing-section landing-problem">
20
+ <div class="landing-container">
21
+ <h2 class="landing-section-title"><%= t('collavre.landing.problem.title') %></h2>
22
+ <div class="landing-problem-grid">
23
+ <div class="landing-problem-card">
24
+ <span class="landing-problem-icon">📝</span>
25
+ <h3><%= t('collavre.landing.problem.notion.title') %></h3>
26
+ <p><%= t('collavre.landing.problem.notion.desc') %></p>
27
+ </div>
28
+ <div class="landing-problem-card">
29
+ <span class="landing-problem-icon">📋</span>
30
+ <h3><%= t('collavre.landing.problem.jira.title') %></h3>
31
+ <p><%= t('collavre.landing.problem.jira.desc') %></p>
32
+ </div>
33
+ <div class="landing-problem-card">
34
+ <span class="landing-problem-icon">💬</span>
35
+ <h3><%= t('collavre.landing.problem.slack.title') %></h3>
36
+ <p><%= t('collavre.landing.problem.slack.desc') %></p>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </section>
41
+
42
+ <%# ═══════════════════════════════════════ FEATURES ═════════════════════════════════════════ %>
43
+ <section id="features" class="landing-section landing-features">
44
+ <div class="landing-container">
45
+ <h2 class="landing-section-title"><%= t('collavre.landing.features.title') %></h2>
46
+ <p class="landing-section-sub"><%= t('collavre.landing.features.subtitle') %></p>
47
+
48
+ <div class="landing-features-grid">
49
+ <div class="landing-feature-card">
50
+ <div class="landing-feature-icon">🌳</div>
51
+ <h3><%= t('collavre.landing.features.tree.title') %></h3>
52
+ <p><%= t('collavre.landing.features.tree.desc') %></p>
53
+ </div>
54
+
55
+ <div class="landing-feature-card">
56
+ <div class="landing-feature-icon">🤖</div>
57
+ <h3><%= t('collavre.landing.features.ai.title') %></h3>
58
+ <p><%= t('collavre.landing.features.ai.desc') %></p>
59
+ </div>
60
+
61
+ <div class="landing-feature-card">
62
+ <div class="landing-feature-icon">💡</div>
63
+ <h3><%= t('collavre.landing.features.context.title') %></h3>
64
+ <p><%= t('collavre.landing.features.context.desc') %></p>
65
+ </div>
66
+
67
+ <div class="landing-feature-card">
68
+ <div class="landing-feature-icon">💬</div>
69
+ <h3><%= t('collavre.landing.features.chat.title') %></h3>
70
+ <p><%= t('collavre.landing.features.chat.desc') %></p>
71
+ </div>
72
+
73
+
74
+ </div>
75
+ </div>
76
+ </section>
77
+
78
+ <%# ══════════════════════════════════════════ DEMO ══════════════════════════════════════════ %>
79
+ <section id="demo" class="landing-section landing-demo">
80
+ <div class="landing-container">
81
+ <h2 class="landing-section-title"><%= t('collavre.landing.demo.title') %></h2>
82
+ <%# Light mode video %>
83
+ <div class="landing-demo-frame landing-demo-light" data-controller="landing-video">
84
+ <video class="landing-demo-video"
85
+ data-landing-video-target="video"
86
+ autoplay muted loop playsinline
87
+ poster="/public-assets/blobs/eyJfcmFpbHMiOnsiZGF0YSI6NDEyLCJwdXIiOiJibG9iX2lkIn19--33811afcd451cef0cf10313fa1bfecca15b41299/demo-poster.jpg">
88
+ <source src="/public-assets/blobs/eyJfcmFpbHMiOnsiZGF0YSI6NDEwLCJwdXIiOiJibG9iX2lkIn19--c272341a19ed8e847208c52da0617ebc801e2e67/demo.mp4" type="video/mp4">
89
+ </video>
90
+ <div class="landing-demo-progress" data-landing-video-target="progressBar">
91
+ <div class="landing-demo-progress-fill" data-landing-video-target="progressFill"></div>
92
+ </div>
93
+ <button class="landing-demo-toggle" data-landing-video-target="toggle" data-action="click->landing-video#togglePlay">
94
+ <span class="landing-demo-toggle-icon" data-landing-video-target="icon">❚❚</span>
95
+ </button>
96
+ </div>
97
+ <%# Dark mode video %>
98
+ <div class="landing-demo-frame landing-demo-dark" data-controller="landing-video">
99
+ <video class="landing-demo-video"
100
+ data-landing-video-target="video"
101
+ autoplay muted loop playsinline
102
+ poster="/public-assets/blobs/eyJfcmFpbHMiOnsiZGF0YSI6NDEzLCJwdXIiOiJibG9iX2lkIn19--53c3c505596bca0befbbebb5958764a153ffee5e/demo-dark-poster.jpg">
103
+ <source src="/public-assets/blobs/eyJfcmFpbHMiOnsiZGF0YSI6NDExLCJwdXIiOiJibG9iX2lkIn19--758c90c311bb68f46a2bda87553e3cdfd800420b/demo-dark.mp4" type="video/mp4">
104
+ </video>
105
+ <div class="landing-demo-progress" data-landing-video-target="progressBar">
106
+ <div class="landing-demo-progress-fill" data-landing-video-target="progressFill"></div>
107
+ </div>
108
+ <button class="landing-demo-toggle" data-landing-video-target="toggle" data-action="click->landing-video#togglePlay">
109
+ <span class="landing-demo-toggle-icon" data-landing-video-target="icon">❚❚</span>
110
+ </button>
111
+ </div>
112
+ </div>
113
+ </section>
114
+
115
+ <%# ═══════════════════════════════════════ FINAL CTA ════════════════════════════════════════ %>
116
+ <section class="landing-section landing-cta">
117
+ <div class="landing-container">
118
+ <h2 class="landing-cta-title"><%= t('collavre.landing.cta.title') %></h2>
119
+ <p class="landing-cta-sub"><%= t('collavre.landing.cta.subtitle') %></p>
120
+ <%= link_to t('collavre.landing.cta.start'), collavre.new_user_path, class: "landing-btn landing-btn-lg" %>
121
+ </div>
122
+ </section>
123
+
124
+ <%# ═══════════════════════════════════════ FOOTER ═══════════════════════════════════════════ %>
125
+ <footer class="landing-footer">
126
+ <div class="landing-container landing-footer-inner">
127
+ <p class="landing-footer-copy">© <%= Date.today.year %> <%= t('app.name') %>. <%= t('collavre.landing.footer.rights') %></p>
128
+ </div>
129
+ </footer>
130
+ </div>
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= t('app.name') %> — <%= t('collavre.landing.meta.title') %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="description" content="<%= t('collavre.landing.meta.description') %>">
7
+ <meta property="og:title" content="<%= t('app.name') %> — <%= t('collavre.landing.meta.title') %>">
8
+ <meta property="og:description" content="<%= t('collavre.landing.meta.description') %>">
9
+ <%= csrf_meta_tags %>
10
+ <%= csp_meta_tag %>
11
+
12
+ <link rel="icon" href="/icon-1e3cf549d2.png" type="image/png">
13
+ <link rel="icon" href="/icon-1e3cf549d2.svg" type="image/svg+xml">
14
+ <link rel="apple-touch-icon" href="/icon-1e3cf549d2.png">
15
+
16
+ <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
17
+ <%= collavre_stylesheets %>
18
+ <%= stylesheet_link_tag "collavre/landing", "data-turbo-track": "reload" %>
19
+
20
+ <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true, type: "module" %>
21
+
22
+ <%# Admin-configured default themes for users without personal theme %>
23
+ <% default_styles = default_theme_styles %>
24
+ <% if default_styles.present? %>
25
+ <style id="default-theme-styles" data-turbo-track="reload"><%= default_styles %></style>
26
+ <% end %>
27
+ </head>
28
+ <body class="<%= body_theme_class %> landing-page">
29
+ <main>
30
+ <%= yield %>
31
+ </main>
32
+ </body>
33
+ </html>
@@ -13,8 +13,10 @@ en:
13
13
  mcp_tool_approval_hint: "Require system administrator approval for new MCP tools."
14
14
  creatives_login_required: "Require Login for Creatives"
15
15
  creatives_login_required_hint: "If checked, users must be logged in to view /creatives."
16
- home_page_path: "Home Page Path"
17
- home_page_path_hint: "Path to render when accessing '/'. The URL stays as '/' but shows content from the specified path. Leave empty to use default (/creatives). Examples: /creatives, /user"
16
+ home_page_path: "Home Page Path (Unauthenticated)"
17
+ home_page_path_hint: "Path to render for visitors who hit '/' while signed out. The URL stays as '/' but the response shows content from the specified path. Leave empty to use default (/creatives). Examples: /landing, /creatives"
18
+ home_page_path_authenticated: "Home Page Path (Authenticated)"
19
+ home_page_path_authenticated_hint: "Signed-in users who hit '/' are redirected to this path. Unlike the unauthenticated path, the URL actually changes. Leave empty to use default (/creatives). Set to '/' to disable the redirect and fall back to the unauthenticated path above. Examples: /creatives, /dashboard"
18
20
  home_page_path_invalid_url: "Home page path cannot be a full URL. Please enter a path like /creatives or /user."
19
21
  home_page_path_not_routable: "Home page path '%{path}' is not a valid route. Please enter an existing path."
20
22
  home_page_path_not_html: "Home page path '%{path}' does not serve HTML content. Please enter a path to an HTML page."
@@ -13,8 +13,10 @@ ko:
13
13
  mcp_tool_approval_hint: "새 MCP 도구 등록 시 시스템 관리자의 승인이 필요합니다."
14
14
  creatives_login_required: "Creatives 로그인 필요"
15
15
  creatives_login_required_hint: "체크 시, 로그인을 한 사용자만 /creatives 에 접근할 수 있습니다."
16
- home_page_path: "홈 페이지 경로"
17
- home_page_path_hint: "'/' 접속 시 표시할 경로입니다. URL은 '/'로 유지되며 지정된 경로의 콘텐츠가 표시됩니다. 비워두면 기본값(/creatives)을 사용합니다. 예: /creatives, /user"
16
+ home_page_path: "홈 페이지 경로 (비로그인)"
17
+ home_page_path_hint: "비로그인 사용자가 '/' 접속 시 표시할 경로입니다. URL은 '/'로 유지되며 지정된 경로의 콘텐츠가 표시됩니다. 비워두면 기본값(/creatives)을 사용합니다. 예: /landing, /creatives"
18
+ home_page_path_authenticated: "홈 페이지 경로 (로그인)"
19
+ home_page_path_authenticated_hint: "로그인 사용자가 '/' 접속 시 이 경로로 리다이렉트됩니다. 비로그인 경로와 달리 URL이 실제로 변경됩니다. 비워두면 기본값(/creatives)을 사용합니다. '/'로 설정하면 리다이렉트하지 않고 위의 비로그인 경로 설정을 따릅니다. 예: /creatives, /dashboard"
18
20
  home_page_path_invalid_url: "홈 페이지 경로는 전체 URL이 될 수 없습니다. /creatives 또는 /user와 같은 경로를 입력하세요."
19
21
  home_page_path_not_routable: "홈 페이지 경로 '%{path}'는 유효한 라우트가 아닙니다. 존재하는 경로를 입력하세요."
20
22
  home_page_path_not_html: "홈 페이지 경로 '%{path}'는 HTML 콘텐츠를 제공하지 않습니다. HTML 페이지 경로를 입력하세요."
@@ -0,0 +1,11 @@
1
+ en:
2
+ collavre:
3
+ channels:
4
+ detach: "Detach"
5
+ channel:
6
+ preview:
7
+ label_default: "Preview"
8
+ attached_message: "%{label} server started.\n\n%{url}"
9
+ badge:
10
+ running: "Running"
11
+ stopped: "Stopped"
@@ -0,0 +1,11 @@
1
+ ko:
2
+ collavre:
3
+ channels:
4
+ detach: "해제"
5
+ channel:
6
+ preview:
7
+ label_default: "프리뷰"
8
+ attached_message: "%{label} 서버가 시작되었습니다.\n\n%{url}"
9
+ badge:
10
+ running: "실행 중"
11
+ stopped: "중지됨"
@@ -211,6 +211,8 @@ en:
211
211
  delete: Delete
212
212
  delete_confirm: Delete this image?
213
213
  counter: "%{current} / %{total}"
214
+ channels:
215
+ detach: "Detach"
214
216
  calendar_events:
215
217
  deleted: Calendar event deleted.
216
218
  google_calendar:
@@ -208,6 +208,8 @@ ko:
208
208
  delete: 삭제
209
209
  delete_confirm: 이 이미지를 삭제하시겠습니까?
210
210
  counter: "%{current} / %{total}"
211
+ channels:
212
+ detach: "해제"
211
213
  calendar_events:
212
214
  deleted: 캘린더 이벤트가 삭제되었습니다.
213
215
  google_calendar: