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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/actiontext.css +92 -2
- data/app/assets/stylesheets/collavre/code_highlight.css +144 -26
- data/app/assets/stylesheets/collavre/comments_popup.css +83 -0
- data/app/assets/stylesheets/collavre/landing.css +507 -0
- data/app/channels/collavre/comments_presence_channel.rb +7 -0
- data/app/controllers/collavre/admin/integrations_controller.rb +82 -0
- data/app/controllers/collavre/admin/settings_controller.rb +22 -17
- data/app/controllers/collavre/application_controller.rb +27 -0
- data/app/controllers/collavre/channels_controller.rb +23 -0
- data/app/controllers/collavre/creatives_controller.rb +50 -6
- data/app/controllers/collavre/landing_controller.rb +8 -0
- data/app/controllers/collavre/passwords_controller.rb +1 -0
- data/app/controllers/collavre/public_assets_controller.rb +24 -0
- data/app/controllers/collavre/topics_controller.rb +21 -30
- data/app/helpers/collavre/comments_helper.rb +7 -0
- data/app/helpers/collavre/public_assets_helper.rb +14 -0
- data/app/javascript/controllers/comment_controller.js +9 -0
- data/app/javascript/controllers/comments/form_controller.js +4 -0
- data/app/javascript/controllers/comments/list_controller.js +10 -7
- data/app/javascript/controllers/comments/popup_controller.js +9 -0
- data/app/javascript/controllers/comments/presence_controller.js +83 -1
- data/app/javascript/controllers/comments/topics_controller.js +15 -0
- data/app/javascript/controllers/creatives/__tests__/sync_controller.test.js +89 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +120 -0
- data/app/javascript/controllers/creatives/sync_controller.js +30 -9
- data/app/javascript/controllers/creatives/tree_controller.js +23 -0
- data/app/javascript/controllers/index.js +4 -1
- data/app/javascript/controllers/landing_video_controller.js +53 -0
- data/app/javascript/creatives/tree_renderer.js +6 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +27 -0
- data/app/javascript/lib/api/queue_manager.js +17 -5
- data/app/javascript/modules/__tests__/command_args_form.test.js +103 -0
- data/app/javascript/modules/__tests__/html_content_empty.test.js +41 -0
- data/app/javascript/modules/__tests__/markdown_source_reconcile.test.js +70 -0
- data/app/javascript/modules/command_args_form.js +22 -4
- data/app/javascript/modules/command_menu.js +27 -0
- data/app/javascript/modules/creative_row_editor.js +227 -17
- data/app/javascript/modules/html_content_empty.js +12 -0
- data/app/javascript/modules/markdown_source_reconcile.js +53 -0
- data/app/jobs/collavre/drop_trigger_job.rb +37 -8
- data/app/mailers/collavre/application_mailer.rb +1 -1
- data/app/models/collavre/channel/injected_message.rb +5 -0
- data/app/models/collavre/channel.rb +87 -0
- data/app/models/collavre/creative/describable.rb +65 -3
- data/app/models/collavre/creative.rb +2 -0
- data/app/models/collavre/integration_setting.rb +35 -0
- data/app/models/collavre/preview_channel.rb +93 -0
- data/app/models/collavre/system_setting.rb +13 -2
- data/app/models/collavre/topic.rb +3 -25
- data/app/models/concerns/collavre/ai_agent_resolvable.rb +12 -3
- data/app/services/collavre/ai_client.rb +3 -3
- data/app/services/collavre/channel_attacher.rb +58 -0
- data/app/services/collavre/comments/mcp_command.rb +31 -1
- data/app/services/collavre/creatives/tree_builder.rb +7 -3
- data/app/services/collavre/google_calendar_service.rb +4 -2
- data/app/services/collavre/markdown_converter.rb +130 -15
- data/app/services/collavre/markdown_importer.rb +7 -2
- data/app/services/collavre/orchestration/policy_resolver.rb +11 -1
- data/app/services/collavre/tools/creative_attach_files_service.rb +96 -0
- data/app/services/collavre/tools/creative_list_attachments_service.rb +42 -0
- data/app/services/collavre/tools/creative_remove_attachment_service.rb +35 -0
- data/app/services/collavre/tools/permission_denied_error.rb +9 -0
- data/app/services/collavre/tools/preview_attach_service.rb +128 -0
- data/app/services/collavre/tools/preview_detach_service.rb +61 -0
- data/app/services/collavre/tools/topic_authorizer.rb +24 -0
- data/app/services/collavre/topic_branch_service.rb +34 -26
- data/app/views/admin/shared/_tabs.html.erb +1 -0
- data/app/views/collavre/admin/integrations/_category.html.erb +22 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +54 -0
- data/app/views/collavre/admin/integrations/index.html.erb +42 -0
- data/app/views/collavre/admin/settings/_system_tab.html.erb +8 -0
- data/app/views/collavre/comments/_channel_chips.html.erb +33 -0
- data/app/views/collavre/comments/_comment.html.erb +6 -1
- data/app/views/collavre/comments/_comments_popup.html.erb +1 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +19 -0
- data/app/views/collavre/creatives/index.html.erb +10 -2
- data/app/views/collavre/landing/show.html.erb +130 -0
- data/app/views/layouts/collavre/landing.html.erb +33 -0
- data/config/locales/admin.en.yml +4 -2
- data/config/locales/admin.ko.yml +4 -2
- data/config/locales/channels.en.yml +11 -0
- data/config/locales/channels.ko.yml +11 -0
- data/config/locales/comments.en.yml +2 -0
- data/config/locales/comments.ko.yml +2 -0
- data/config/locales/creatives.en.yml +9 -0
- data/config/locales/creatives.ko.yml +8 -0
- data/config/locales/integrations.en.yml +44 -0
- data/config/locales/integrations.ko.yml +44 -0
- data/config/locales/landing.en.yml +51 -0
- data/config/locales/landing.ko.yml +51 -0
- data/config/routes.rb +18 -0
- data/db/migrate/20260526000000_create_channels.rb +42 -0
- data/db/migrate/20260527000000_add_dismissed_at_to_channels.rb +6 -0
- data/db/migrate/20260527000100_backfill_dismissed_at_for_legacy_detached_channels.rb +28 -0
- data/db/migrate/20260528000000_add_preview_channel_unique_index.rb +31 -0
- data/db/migrate/20260529000000_add_primary_agent_id_to_topics.rb +40 -0
- data/db/migrate/20260529100000_create_integration_settings.rb +15 -0
- data/db/seeds.rb +19 -0
- data/lib/collavre/aws_credentials.rb +75 -0
- data/lib/collavre/engine.rb +51 -0
- data/lib/collavre/integration_settings/key_definition.rb +29 -0
- data/lib/collavre/integration_settings/registry.rb +55 -0
- data/lib/collavre/integration_settings/resolver.rb +71 -0
- data/lib/collavre/integration_settings.rb +46 -0
- data/lib/collavre/ses_settings_interceptor.rb +72 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +3 -0
- 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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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') %>">×</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
|
-
|
|
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') %>">⌃</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":
|
|
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>
|
data/config/locales/admin.en.yml
CHANGED
|
@@ -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
|
|
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."
|
data/config/locales/admin.ko.yml
CHANGED
|
@@ -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)을 사용합니다. 예: /
|
|
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 페이지 경로를 입력하세요."
|