collavre 0.21.0 โ 0.23.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/README.md +1 -0
- data/app/assets/stylesheets/collavre/actiontext.css +251 -90
- data/app/assets/stylesheets/collavre/code_highlight.css +7 -201
- data/app/assets/stylesheets/collavre/comments_popup.css +169 -64
- data/app/assets/stylesheets/collavre/creatives.css +11 -2
- data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +148 -0
- data/app/assets/stylesheets/collavre/tables.css +91 -0
- data/app/channels/collavre/agent_channel.rb +205 -0
- data/app/channels/collavre/inbox_badge_channel.rb +30 -0
- data/app/controllers/collavre/admin/integrations_controller.rb +16 -5
- data/app/controllers/collavre/api/v1/agents_controller.rb +777 -0
- data/app/controllers/collavre/api/v1/base_controller.rb +46 -0
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +224 -0
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +95 -0
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +31 -0
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +25 -0
- data/app/controllers/collavre/attachments_controller.rb +30 -2
- data/app/controllers/collavre/comments_controller.rb +1 -1
- data/app/controllers/collavre/creatives/attachments_controller.rb +79 -0
- data/app/controllers/collavre/creatives_controller.rb +107 -6
- data/app/controllers/collavre/tasks_controller.rb +21 -4
- data/app/controllers/collavre/topics_controller.rb +64 -1
- data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +57 -14
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +16 -1
- data/app/controllers/concerns/collavre/users_controller/registration.rb +41 -1
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/javascript/collavre.js +2 -0
- data/app/javascript/components/ImageResizer.jsx +9 -3
- data/app/javascript/components/InlineLexicalEditor.jsx +196 -96
- data/app/javascript/components/creative_tree_row.js +20 -3
- data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +3 -0
- data/app/javascript/components/plugins/image_upload_plugin.jsx +12 -0
- data/app/javascript/components/plugins/list_tab_indent_plugin.jsx +16 -0
- data/app/javascript/components/plugins/table_hover_actions_plugin.jsx +405 -0
- data/app/javascript/controllers/__tests__/inbox_badge_controller.test.js +73 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +447 -0
- data/app/javascript/controllers/comment_controller.js +11 -5
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/__tests__/form_controller_double_submit.test.js +159 -0
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +111 -2
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +94 -0
- data/app/javascript/controllers/comments/form_controller.js +21 -5
- data/app/javascript/controllers/comments/list_controller.js +35 -19
- data/app/javascript/controllers/comments/presence_controller.js +58 -6
- data/app/javascript/controllers/comments/topics_controller.js +14 -8
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +150 -0
- data/app/javascript/controllers/creatives/import_controller.js +2 -1
- data/app/javascript/controllers/creatives/select_mode_controller.js +2 -1
- data/app/javascript/controllers/creatives/tree_controller.js +142 -1
- data/app/javascript/controllers/image_lightbox_controller.js +2 -1
- data/app/javascript/controllers/inbox_badge_controller.js +33 -0
- data/app/javascript/controllers/index.js +4 -1
- data/app/javascript/controllers/link_creative_controller.js +451 -29
- data/app/javascript/controllers/share_modal_controller.js +4 -3
- data/app/javascript/controllers/topic_search_controller.js +2 -1
- data/app/javascript/creatives/drag_drop/event_handlers.js +14 -5
- data/app/javascript/creatives/topic_move_members_popup.js +156 -0
- data/app/javascript/creatives/tree_renderer.js +11 -0
- data/app/javascript/lib/__tests__/turbo_confirm.test.js +81 -0
- data/app/javascript/lib/__tests__/typo_correction.test.js +192 -0
- data/app/javascript/lib/api/__tests__/api_error.test.js +96 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +88 -1
- data/app/javascript/lib/api/api_error.js +108 -0
- data/app/javascript/lib/api/creatives.js +13 -0
- data/app/javascript/lib/api/queue_manager.js +38 -4
- data/app/javascript/lib/common_popup.js +18 -5
- data/app/javascript/lib/editor/__tests__/code_edit_view_token_parity.test.js +121 -0
- data/app/javascript/lib/editor/__tests__/code_language_roundtrip.test.js +152 -0
- data/app/javascript/lib/editor/__tests__/code_languages.test.js +93 -0
- data/app/javascript/lib/editor/code_languages.js +173 -0
- data/app/javascript/lib/editor/code_token_theme.js +41 -0
- data/app/javascript/lib/lexical/__tests__/color_import.test.js +318 -0
- data/app/javascript/lib/lexical/__tests__/image_focus.test.js +139 -0
- data/app/javascript/lib/lexical/__tests__/list_tab_indent.test.js +633 -0
- data/app/javascript/lib/lexical/__tests__/markdown_serialize.test.js +627 -0
- data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +278 -0
- data/app/javascript/lib/lexical/__tests__/selection_boundary.test.js +88 -0
- data/app/javascript/lib/lexical/__tests__/table_transformer.test.js +163 -0
- data/app/javascript/lib/lexical/__tests__/trailing_paragraph.test.js +104 -0
- data/app/javascript/lib/lexical/color_import.js +186 -0
- data/app/javascript/lib/lexical/list_tab_indent.js +210 -0
- data/app/javascript/lib/lexical/markdown_serialize.js +320 -0
- data/app/javascript/lib/lexical/minimize_html.js +182 -0
- data/app/javascript/lib/lexical/selection_boundary.js +58 -0
- data/app/javascript/lib/lexical/table_transformer.js +182 -0
- data/app/javascript/lib/lexical/trailing_paragraph.js +29 -0
- data/app/javascript/lib/lexical/video_node.jsx +96 -0
- data/app/javascript/lib/turbo_confirm.js +46 -0
- data/app/javascript/lib/typo_correction.js +146 -0
- data/app/javascript/lib/utils/__tests__/confirm_dialog.test.js +88 -0
- data/app/javascript/lib/utils/__tests__/dialog.test.js +92 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +153 -0
- data/app/javascript/lib/utils/__tests__/sanitize_description.test.js +68 -0
- data/app/javascript/lib/utils/__tests__/table_download.test.js +93 -0
- data/app/javascript/lib/utils/confirm_dialog.js +10 -0
- data/app/javascript/lib/utils/dialog.js +300 -0
- data/app/javascript/lib/utils/markdown.js +154 -67
- data/app/javascript/lib/utils/sanitize_description.js +31 -0
- data/app/javascript/lib/utils/table_download.js +15 -0
- data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
- data/app/javascript/modules/creative_row_editor.js +110 -70
- data/app/javascript/modules/export_to_markdown.js +2 -1
- data/app/javascript/modules/lexical_inline_editor.jsx +2 -1
- data/app/javascript/modules/slide_view.js +11 -2
- data/app/javascript/modules/typo_corrector.js +534 -0
- data/app/jobs/collavre/ai_agent_job.rb +92 -3
- data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +134 -0
- data/app/jobs/collavre/claude_channel_presence_job.rb +91 -0
- data/app/jobs/collavre/compress_job.rb +6 -2
- data/app/models/collavre/agent_subscription.rb +52 -0
- data/app/models/collavre/comment/broadcastable.rb +46 -7
- data/app/models/collavre/comment/claude_channel_permission.rb +145 -0
- data/app/models/collavre/comment/notifiable.rb +14 -4
- data/app/models/collavre/comment.rb +124 -11
- data/app/models/collavre/creative/describable.rb +220 -4
- data/app/models/collavre/creative_share.rb +1 -0
- data/app/models/collavre/task.rb +49 -5
- data/app/models/collavre/topic.rb +5 -0
- data/app/models/collavre/user.rb +61 -1
- data/app/services/collavre/agent_session_abort.rb +28 -0
- data/app/services/collavre/ai_agent/claude_channel_adapter.rb +79 -0
- data/app/services/collavre/ai_agent/response_finalizer.rb +4 -2
- data/app/services/collavre/ai_agent_service.rb +68 -49
- data/app/services/collavre/ai_client.rb +28 -10
- data/app/services/collavre/attachment_backfill.rb +26 -0
- data/app/services/collavre/auto_theme_generator.rb +1 -1
- data/app/services/collavre/creatives/breadcrumb_resolver.rb +91 -0
- data/app/services/collavre/creatives/filter_pipeline.rb +26 -42
- data/app/services/collavre/creatives/filters/search_filter.rb +12 -2
- data/app/services/collavre/creatives/index_query.rb +195 -24
- data/app/services/collavre/creatives/permission_filter.rb +50 -0
- data/app/services/collavre/creatives/reveal_path_resolver.rb +118 -0
- data/app/services/collavre/creatives/tree_builder.rb +2 -1
- data/app/services/collavre/crons/recurring_task_arguments.rb +28 -0
- data/app/services/collavre/gemini_parent_recommender.rb +1 -1
- data/app/services/collavre/inbox_reply_service.rb +5 -0
- data/app/services/collavre/markdown_converter.rb +13 -3
- data/app/services/collavre/mobile/event_summarizer.rb +40 -0
- data/app/services/collavre/orchestration/agent_orchestrator.rb +33 -7
- data/app/services/collavre/orchestration/arbiter.rb +16 -0
- data/app/services/collavre/orchestration/matcher.rb +79 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +4 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +146 -19
- data/app/services/collavre/tools/creative_attach_files_service.rb +29 -63
- data/app/services/collavre/tools/creative_batch_service.rb +3 -2
- data/app/services/collavre/tools/creative_create_service.rb +8 -8
- data/app/services/collavre/tools/creative_remove_attachment_service.rb +7 -5
- data/app/services/collavre/tools/creative_update_service.rb +23 -8
- data/app/services/collavre/tools/cron_list_service.rb +1 -14
- data/app/services/collavre/topics/orphaned_cron_notifier.rb +68 -0
- data/app/services/collavre/typo_corrector.rb +188 -0
- data/app/views/collavre/admin/integrations/_category.html.erb +1 -1
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +27 -11
- data/app/views/collavre/comments/_comment.html.erb +15 -1
- data/app/views/collavre/comments/_comments_popup.html.erb +18 -3
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +1 -0
- data/app/views/collavre/creatives/_topic_move_members_modal.html.erb +42 -0
- data/app/views/collavre/creatives/index.html.erb +14 -1
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -2
- data/app/views/collavre/users/show.html.erb +3 -0
- data/app/views/collavre/users/typo_correction.html.erb +50 -0
- data/app/views/layouts/collavre/slide.html.erb +1 -0
- data/config/locales/channels.en.yml +2 -0
- data/config/locales/channels.ko.yml +2 -0
- data/config/locales/claude_channel.en.yml +16 -0
- data/config/locales/claude_channel.ko.yml +16 -0
- data/config/locales/comments.en.yml +18 -0
- data/config/locales/comments.ko.yml +18 -0
- data/config/locales/creatives.en.yml +2 -0
- data/config/locales/creatives.ko.yml +2 -0
- data/config/locales/integrations.en.yml +13 -2
- data/config/locales/integrations.ko.yml +13 -2
- data/config/locales/mobile.en.yml +16 -0
- data/config/locales/mobile.ko.yml +16 -0
- data/config/locales/orchestration.en.yml +1 -0
- data/config/locales/orchestration.ko.yml +1 -0
- data/config/locales/users.en.yml +15 -0
- data/config/locales/users.ko.yml +15 -0
- data/config/routes.rb +25 -0
- data/db/migrate/20260609000000_drop_action_text_rich_texts.rb +20 -0
- data/db/migrate/20260609005000_add_session_id_to_topics.rb +16 -0
- data/db/migrate/20260609010000_create_agent_subscriptions.rb +19 -0
- data/db/migrate/20260609020000_add_last_seen_at_to_agent_subscriptions.rb +23 -0
- data/db/migrate/20260609030000_add_session_id_to_agent_subscriptions.rb +17 -0
- data/db/migrate/20260609190659_backfill_creative_files_into_description.rb +24 -0
- data/db/migrate/20260612000000_add_topic_concurrency_defer_to_comments.rb +38 -0
- data/db/migrate/20260617090000_add_typo_correction_settings_to_users.rb +18 -0
- data/db/seeds.rb +51 -0
- data/lib/collavre/engine.rb +0 -1
- data/lib/collavre/integration_settings/key_definition.rb +6 -0
- data/lib/collavre/integration_settings/registry.rb +7 -2
- data/lib/collavre/version.rb +1 -1
- data/lib/generators/collavre/install/install_generator.rb +1 -0
- metadata +85 -3
- data/app/services/collavre/openclaw_abort_service.rb +0 -45
- data/app/services/collavre/tools/description_normalizable.rb +0 -16
|
@@ -5,6 +5,7 @@ module Tools
|
|
|
5
5
|
class CronListService
|
|
6
6
|
extend T::Sig
|
|
7
7
|
extend ToolMeta
|
|
8
|
+
include Collavre::Crons::RecurringTaskArguments
|
|
8
9
|
|
|
9
10
|
tool_name "cron_list"
|
|
10
11
|
tool_description "List recurring scheduled jobs. Returns all cron jobs for creatives the current user has access to. Filter by creative_id to see jobs for a specific creative."
|
|
@@ -46,20 +47,6 @@ module Tools
|
|
|
46
47
|
|
|
47
48
|
{ success: true, cron_jobs: results, count: results.size }
|
|
48
49
|
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def parse_arguments(task)
|
|
53
|
-
args = task.arguments
|
|
54
|
-
return {} unless args.is_a?(Array) && args.first.is_a?(Hash)
|
|
55
|
-
|
|
56
|
-
args.first.stringify_keys
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def parse_creative_id_from_key(key)
|
|
60
|
-
match = key.match(/\Acron_(\d+)_/)
|
|
61
|
-
match[1].to_i if match
|
|
62
|
-
end
|
|
63
50
|
end
|
|
64
51
|
end
|
|
65
52
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Topics
|
|
5
|
+
# When a Topic is deleted, find every non-static recurring cron task that
|
|
6
|
+
# targets that topic (topic_id stored inside the task's arguments JSON) and
|
|
7
|
+
# post a system message into the affected creative's Main topic so the user
|
|
8
|
+
# knows the cron is now orphaned.
|
|
9
|
+
#
|
|
10
|
+
# Decision: notify only. The recurring task is intentionally left in place
|
|
11
|
+
# so the user can decide whether to re-point or cancel it.
|
|
12
|
+
class OrphanedCronNotifier
|
|
13
|
+
include Collavre::Crons::RecurringTaskArguments
|
|
14
|
+
|
|
15
|
+
def initialize(topic_id:, topic_name:)
|
|
16
|
+
@topic_id = topic_id
|
|
17
|
+
@topic_name = topic_name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
return if @topic_id.blank?
|
|
22
|
+
|
|
23
|
+
matching_tasks.each do |task, args|
|
|
24
|
+
notify_for(task, args)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def matching_tasks
|
|
31
|
+
SolidQueue::RecurringTask.where(static: false).filter_map do |task|
|
|
32
|
+
args = parse_arguments(task)
|
|
33
|
+
target = args["topic_id"]
|
|
34
|
+
next if target.blank?
|
|
35
|
+
next unless target.to_i == @topic_id.to_i
|
|
36
|
+
|
|
37
|
+
[ task, args ]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def notify_for(task, args)
|
|
42
|
+
creative_id = args["creative_id"] || parse_creative_id_from_key(task.key)
|
|
43
|
+
creative = Creative.find_by(id: creative_id)
|
|
44
|
+
return unless creative
|
|
45
|
+
|
|
46
|
+
# The recurring task stores the original (possibly linked/child) creative
|
|
47
|
+
# id, but Comment#use_origin_creative rewrites the comment to the origin.
|
|
48
|
+
# Post to the origin's Main topic so the notice stays on the origin and
|
|
49
|
+
# is reachable from normal topic navigation (matches CronCreateService).
|
|
50
|
+
main_topic = creative.effective_origin.main_topic
|
|
51
|
+
return unless main_topic
|
|
52
|
+
|
|
53
|
+
Comment.create!(
|
|
54
|
+
creative: creative,
|
|
55
|
+
topic_id: main_topic.id,
|
|
56
|
+
user: nil, # System message
|
|
57
|
+
skip_default_user: true, # keep user nil even when a deleter is Current.user; don't trigger AI orchestration
|
|
58
|
+
content: I18n.t(
|
|
59
|
+
"collavre.topics.orphaned_cron_notice",
|
|
60
|
+
topic_name: @topic_name,
|
|
61
|
+
cron_key: task.key,
|
|
62
|
+
message: args["message"]
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
# Server-side typo correction. Sends the text the user is typing to an LLM and
|
|
3
|
+
# returns a validated *structured edit list* โ never a rewritten string. Each
|
|
4
|
+
# edit is {original, suggestion, reason, confidence}. Validation rejects anything
|
|
5
|
+
# that is not a small, in-place spelling fix (style rewrites, hallucinated spans,
|
|
6
|
+
# edits inside code/URLs/mentions), so the UI can trust every returned edit.
|
|
7
|
+
class TypoCorrector
|
|
8
|
+
AGENT_EMAIL = "typo-corrector@collavre.local"
|
|
9
|
+
|
|
10
|
+
# Reject "corrections" that are really rewrites: an edit is kept only when the
|
|
11
|
+
# original is short and the suggestion is within this edit distance of it.
|
|
12
|
+
MAX_ORIGINAL_LENGTH = 40
|
|
13
|
+
MIN_EDIT_DISTANCE_CAP = 2
|
|
14
|
+
EDIT_DISTANCE_RATIO = 0.4
|
|
15
|
+
|
|
16
|
+
FALLBACK_SYSTEM_PROMPT = <<~PROMPT.freeze
|
|
17
|
+
You are a typo correction engine. Fix only spelling mistakes and obvious typos,
|
|
18
|
+
never style, grammar or meaning, in any language. `original` must be an exact
|
|
19
|
+
substring of the input. Return ONLY JSON:
|
|
20
|
+
{"edits":[{"original":"x","suggestion":"y","reason":"spelling","confidence":0.0}]}
|
|
21
|
+
PROMPT
|
|
22
|
+
|
|
23
|
+
def initialize(client: nil)
|
|
24
|
+
@client = client
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns an Array of Hashes with string keys: "original", "suggestion",
|
|
28
|
+
# "reason", "confidence". Empty array on blank input or LLM/parse failure.
|
|
29
|
+
def correct(text)
|
|
30
|
+
text = text.to_s
|
|
31
|
+
return [] if text.strip.blank?
|
|
32
|
+
|
|
33
|
+
response = client.chat([
|
|
34
|
+
{ role: :user, parts: [ { text: "Text:\n#{text}" } ] }
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
edits = parse_response(response)
|
|
38
|
+
validate(edits, text)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def client
|
|
44
|
+
@client ||= build_client
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_client
|
|
48
|
+
agent = Collavre.user_class.find_by(email: AGENT_EMAIL)
|
|
49
|
+
AiClient.new(
|
|
50
|
+
vendor: agent&.llm_vendor.presence || ENV.fetch("COLLAVRE_DEFAULT_LLM_VENDOR", "gemini"),
|
|
51
|
+
model: agent&.llm_model.presence || ENV.fetch("COLLAVRE_DEFAULT_LLM_MODEL", "gemini-3.1-flash-lite"),
|
|
52
|
+
system_prompt: agent&.system_prompt.presence || FALLBACK_SYSTEM_PROMPT,
|
|
53
|
+
llm_api_key: agent&.llm_api_key,
|
|
54
|
+
gateway_url: agent&.gateway_url,
|
|
55
|
+
# Vendor adapters (e.g. OpenClaw) resolve the gateway/key from the context
|
|
56
|
+
# user, not the llm_api_key/gateway_url kwargs. Pass the agent so an
|
|
57
|
+
# OpenClaw-backed typo agent isn't built with user: nil (which makes the
|
|
58
|
+
# adapter bail with "Gateway URL not configured" and silently return no edits).
|
|
59
|
+
context: { user: agent },
|
|
60
|
+
# Runs on debounced typing of *unsubmitted* text โ never persist the draft
|
|
61
|
+
# to ActivityLog (RubyLlmInteractionLogger writes `messages` there).
|
|
62
|
+
log_interactions: false
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_response(content)
|
|
67
|
+
return [] if content.blank?
|
|
68
|
+
|
|
69
|
+
cleaned = content.gsub(/^```json\s*/, "").gsub(/\s*```$/, "").strip
|
|
70
|
+
parsed = JSON.parse(cleaned)
|
|
71
|
+
edits = parsed.is_a?(Hash) ? parsed["edits"] : parsed
|
|
72
|
+
edits.is_a?(Array) ? edits : []
|
|
73
|
+
rescue JSON::ParserError => e
|
|
74
|
+
# Never log the raw response: it can echo the user's unsubmitted draft,
|
|
75
|
+
# which would leak private text to application logs despite log_interactions: false.
|
|
76
|
+
Rails.logger.warn("[TypoCorrector] JSON parse error: #{e.message} (#{content.to_s.length} chars)")
|
|
77
|
+
[]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate(edits, text)
|
|
81
|
+
skip_ranges = skip_ranges(text)
|
|
82
|
+
seen = {}
|
|
83
|
+
|
|
84
|
+
edits.filter_map do |edit|
|
|
85
|
+
next unless edit.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
original = edit["original"].to_s
|
|
88
|
+
suggestion = edit["suggestion"].to_s
|
|
89
|
+
next if original.empty? || suggestion.empty? || original == suggestion
|
|
90
|
+
next if original.length > MAX_ORIGINAL_LENGTH
|
|
91
|
+
|
|
92
|
+
# `original` must appear verbatim in the text, outside any skip region
|
|
93
|
+
# (code/URL/mention/markup). We return the offset of that first *valid*
|
|
94
|
+
# occurrence so the client anchors there โ otherwise the client's own
|
|
95
|
+
# left-to-right search would bind to an earlier occurrence sitting inside
|
|
96
|
+
# a protected span and edit it (a markdown-canonical/code-safety hole).
|
|
97
|
+
offset = first_offset_outside_skip_ranges(text, original, skip_ranges)
|
|
98
|
+
next if offset.nil?
|
|
99
|
+
|
|
100
|
+
# Reject big rewrites โ keep only minimal in-place fixes.
|
|
101
|
+
cap = [ MIN_EDIT_DISTANCE_CAP, (original.length * EDIT_DISTANCE_RATIO).ceil ].max
|
|
102
|
+
next if levenshtein(original, suggestion) > cap
|
|
103
|
+
|
|
104
|
+
key = [ original, suggestion ]
|
|
105
|
+
next if seen[key]
|
|
106
|
+
seen[key] = true
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
"original" => original,
|
|
110
|
+
"suggestion" => suggestion,
|
|
111
|
+
"reason" => edit["reason"].presence || "spelling",
|
|
112
|
+
"confidence" => clamp_confidence(edit["confidence"]),
|
|
113
|
+
# Emit the offset in UTF-16 code units, the coordinate space JS string
|
|
114
|
+
# indexing uses. A Ruby character index would be off by one per astral
|
|
115
|
+
# character (emoji, etc.) before the typo, so the client's substring
|
|
116
|
+
# check at `offset` would fail and fall back to indexOf โ which can bind
|
|
117
|
+
# to a protected occurrence the server already excluded.
|
|
118
|
+
"offset" => utf16_offset(text, offset)
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def clamp_confidence(value)
|
|
124
|
+
return 0.5 if value.nil?
|
|
125
|
+
|
|
126
|
+
Float(value).clamp(0.0, 1.0)
|
|
127
|
+
rescue ArgumentError, TypeError
|
|
128
|
+
0.5
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Character ranges that must never be edited: fenced code, inline code, URLs,
|
|
132
|
+
# @mentions, HTML tags, and markdown-canonical <span style> color fragments.
|
|
133
|
+
def skip_ranges(text)
|
|
134
|
+
ranges = []
|
|
135
|
+
patterns = [
|
|
136
|
+
/```.*?```/m, # fenced code blocks
|
|
137
|
+
/`[^`]*`/, # inline code
|
|
138
|
+
%r{https?://\S+}, # URLs
|
|
139
|
+
/@[^\s@]+/, # @mentions
|
|
140
|
+
# `[^<>]` (not just `[^>]`) keeps this linear: a stray run of "<<<<" can't
|
|
141
|
+
# make scan re-walk the tail from every "<" (CodeQL polynomial-ReDoS).
|
|
142
|
+
/<[^<>]+>/ # HTML / span-style markup
|
|
143
|
+
]
|
|
144
|
+
patterns.each do |pattern|
|
|
145
|
+
text.scan(pattern) { ranges << Regexp.last_match.offset(0) }
|
|
146
|
+
end
|
|
147
|
+
ranges
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# First character offset where `needle` occurs entirely outside every skip
|
|
151
|
+
# range, or nil if every occurrence overlaps a protected span.
|
|
152
|
+
def first_offset_outside_skip_ranges(text, needle, skip_ranges)
|
|
153
|
+
pos = 0
|
|
154
|
+
while (idx = text.index(needle, pos))
|
|
155
|
+
finish = idx + needle.length
|
|
156
|
+
inside = skip_ranges.any? { |(s, e)| idx < e && finish > s }
|
|
157
|
+
return idx unless inside
|
|
158
|
+
|
|
159
|
+
pos = idx + 1
|
|
160
|
+
end
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Convert a Ruby character index into a UTF-16 code-unit offset (JS string
|
|
165
|
+
# coordinates). Characters outside the BMP count as two code units in JS.
|
|
166
|
+
def utf16_offset(text, char_index)
|
|
167
|
+
text[0, char_index].to_s.encode("UTF-16LE").bytesize / 2
|
|
168
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
|
169
|
+
char_index
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Iterative Levenshtein distance.
|
|
173
|
+
def levenshtein(a, b)
|
|
174
|
+
a = a.chars
|
|
175
|
+
b = b.chars
|
|
176
|
+
prev = (0..b.length).to_a
|
|
177
|
+
a.each_with_index do |ac, i|
|
|
178
|
+
curr = [ i + 1 ]
|
|
179
|
+
b.each_with_index do |bc, j|
|
|
180
|
+
cost = ac == bc ? 0 : 1
|
|
181
|
+
curr << [ curr[j] + 1, prev[j + 1] + 1, prev[j] + cost ].min
|
|
182
|
+
end
|
|
183
|
+
prev = curr
|
|
184
|
+
end
|
|
185
|
+
prev.last
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
</h3>
|
|
5
5
|
|
|
6
6
|
<div class="table-scroll">
|
|
7
|
-
<table class="settings-table" style="width: 100%; min-width: 640px; border-collapse: collapse;">
|
|
7
|
+
<table class="settings-table" style="width: 100%; min-width: 640px; border-collapse: collapse; table-layout: fixed;">
|
|
8
8
|
<thead>
|
|
9
9
|
<tr>
|
|
10
10
|
<th style="text-align: left; padding: 0.5em; width: 30%;"><%= t("collavre.admin.integrations.headers.key") %></th>
|
|
@@ -8,29 +8,45 @@
|
|
|
8
8
|
end
|
|
9
9
|
current_display = row[:present] ? row[:display_value].to_s : "โ"
|
|
10
10
|
%>
|
|
11
|
+
<% description = t("collavre.admin.integrations.descriptions.#{key_str}", default: "") %>
|
|
11
12
|
<tr style="border-top: 1px solid var(--color-border);">
|
|
12
|
-
<td style="padding: 0.5em; vertical-align: top;">
|
|
13
|
-
<code><%= key_str %></code>
|
|
13
|
+
<td style="padding: 0.5em; vertical-align: top; word-break: break-word; overflow-wrap: anywhere;">
|
|
14
|
+
<code style="word-break: break-all; overflow-wrap: anywhere;"><%= key_str %></code>
|
|
14
15
|
<% if definition.requires_restart %>
|
|
15
16
|
<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
17
|
<%= t("collavre.admin.integrations.restart_required") %>
|
|
17
18
|
</span>
|
|
18
19
|
<% end %>
|
|
20
|
+
<% if description.present? %>
|
|
21
|
+
<div style="font-size: 0.85em; color: var(--color-text); margin-top: 0.4em; line-height: 1.4;">
|
|
22
|
+
<%= simple_format(description) %>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
19
25
|
<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>
|
|
26
|
+
<%= t("collavre.admin.integrations.env_var_label") %>: <code style="word-break: break-all; overflow-wrap: anywhere;"><%= definition.env_var %></code>
|
|
21
27
|
</div>
|
|
22
28
|
</td>
|
|
23
29
|
<td style="padding: 0.5em; vertical-align: top;">
|
|
24
30
|
<%# Input references the bulk form via HTML5 `form` attribute so the table is not nested inside <form>. %>
|
|
25
31
|
<%# Placeholder displays the current value (masked for sensitive); empty input keeps the existing value. %>
|
|
26
|
-
<%
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
<% if definition.input_type == :textarea %>
|
|
33
|
+
<%= text_area_tag "integration_setting[#{key_str}]",
|
|
34
|
+
nil,
|
|
35
|
+
form: bulk_form_id,
|
|
36
|
+
placeholder: current_display,
|
|
37
|
+
autocomplete: "off",
|
|
38
|
+
rows: 8,
|
|
39
|
+
style: "width: 100%; font-family: var(--font-mono, monospace); font-size: 0.85em; min-height: 8em;" %>
|
|
40
|
+
<% else %>
|
|
41
|
+
<% input_tag = definition.sensitive ? :password_field_tag : :text_field_tag %>
|
|
42
|
+
<%= send(input_tag,
|
|
43
|
+
"integration_setting[#{key_str}]",
|
|
44
|
+
nil,
|
|
45
|
+
form: bulk_form_id,
|
|
46
|
+
placeholder: current_display,
|
|
47
|
+
autocomplete: "off",
|
|
48
|
+
style: "width: 100%;") %>
|
|
49
|
+
<% end %>
|
|
34
50
|
<div style="font-size: 0.8em; margin-top: 0.25em;">
|
|
35
51
|
<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
52
|
<%= t("collavre.admin.integrations.source.#{row[:source]}", default: row[:source].to_s.upcase) %>
|
|
@@ -36,7 +36,11 @@
|
|
|
36
36
|
<span class="comment-status-label private-label">๐ <%= t('collavre.comments.private') %></span>
|
|
37
37
|
<% end %>
|
|
38
38
|
<% if comment.action_executed_at.present? %>
|
|
39
|
-
|
|
39
|
+
<% if comment.claude_channel_permission_denied? %>
|
|
40
|
+
<span class="comment-status-label denied-label">๐ซ <%= t('collavre.comments.denied_label') %></span>
|
|
41
|
+
<% else %>
|
|
42
|
+
<span class="comment-status-label approved-label">โ
<%= t('collavre.comments.approved_label') %></span>
|
|
43
|
+
<% end %>
|
|
40
44
|
<% end %>
|
|
41
45
|
<% can_convert_comment = comment.user == Current.user || comment.creative.has_permission?(Current.user, :admin) %>
|
|
42
46
|
</div>
|
|
@@ -45,6 +49,11 @@
|
|
|
45
49
|
<button class="comment-stop-btn" type="button" data-action="click->comment#cancelTask" data-task-id="<%= comment.task_id %>" title="<%= t('collavre.comments.stop_agent') %>">
|
|
46
50
|
<span class="agent-stop-icon">■</span> <%= t('collavre.comments.stop_agent') %>
|
|
47
51
|
</button>
|
|
52
|
+
<% elsif comment.waiting_notice? && (blocking_task = comment.topic_blocking_task) %>
|
|
53
|
+
<%# Stop the in-progress blocker so the deferred waiter can proceed (stuck recovery). %>
|
|
54
|
+
<button class="comment-stop-btn" type="button" data-action="click->comment#cancelTask" data-task-id="<%= blocking_task.id %>" title="<%= t('collavre.comments.stop_blocking_agent') %>">
|
|
55
|
+
<span class="agent-stop-icon">■</span> <%= t('collavre.comments.stop_blocking_agent') %>
|
|
56
|
+
</button>
|
|
48
57
|
<% end %>
|
|
49
58
|
<button class="add-reaction-btn" type="button" data-action="click->comment#triggerReactionPicker" title="<%= t('collavre.comments.add_reaction') %>">
|
|
50
59
|
<span class="grayscale-emoji">โบ</span>
|
|
@@ -57,6 +66,11 @@
|
|
|
57
66
|
<button class="approve-comment-btn comment-approve-hidden" data-comment-target="approveButton" data-comment-id="<%= comment.id %>" title="<%= t('collavre.comments.approve_button') %>">
|
|
58
67
|
<%= t('collavre.comments.approve_button') %>
|
|
59
68
|
</button>
|
|
69
|
+
<% if comment.claude_channel_permission? %>
|
|
70
|
+
<button class="deny-comment-btn comment-approve-hidden" data-comment-target="denyButton" data-comment-id="<%= comment.id %>" title="<%= t('collavre.comments.deny_button') %>">
|
|
71
|
+
<%= t('collavre.comments.deny_button') %>
|
|
72
|
+
</button>
|
|
73
|
+
<% end %>
|
|
60
74
|
<% end %>
|
|
61
75
|
<button class="edit-comment-btn comment-owner-only" data-comment-target="ownerButton" data-comment-id="<%= comment.id %>" data-comment-content="<%= comment.content %>" data-comment-private="<%= comment.private? %>" title="<%= t('collavre.comments.update_comment') %>">
|
|
62
76
|
<%= t('collavre.comments.edit_button') %>
|
|
@@ -61,7 +61,20 @@
|
|
|
61
61
|
data-remove-from-history-label="<%= t('collavre.comments.remove_from_history') %>"
|
|
62
62
|
data-inbox-reply-button="<%= t('collavre.comments.inbox_reply_button') %>"
|
|
63
63
|
data-table-download-csv-text="<%= t('collavre.comments.table_download.csv') %>"
|
|
64
|
-
data-table-download-excel-text="<%= t('collavre.comments.table_download.excel') %>"
|
|
64
|
+
data-table-download-excel-text="<%= t('collavre.comments.table_download.excel') %>"
|
|
65
|
+
<% if Current.user %>
|
|
66
|
+
data-typo-enabled="<%= Current.user.typo_correction_enabled %>"
|
|
67
|
+
data-typo-threshold="<%= Current.user.typo_correction_threshold %>"
|
|
68
|
+
data-typo-on-voice="<%= Current.user.typo_correction_on_voice %>"
|
|
69
|
+
data-typo-on-soft-keyboard="<%= Current.user.typo_correction_on_soft_keyboard %>"
|
|
70
|
+
data-typo-on-physical-keyboard="<%= Current.user.typo_correction_on_physical_keyboard %>"
|
|
71
|
+
data-typo-in-chat="<%= Current.user.typo_correction_in_chat %>"
|
|
72
|
+
data-typo-in-editor="<%= Current.user.typo_correction_in_editor %>"
|
|
73
|
+
data-typo-endpoint="<%= collavre.typo_corrections_path %>"
|
|
74
|
+
data-typo-keep-label="<%= t('collavre.comments.typo_keep_label') %>"
|
|
75
|
+
data-typo-custom-label="<%= t('collavre.comments.typo_custom_label') %>"
|
|
76
|
+
data-typo-input-label="<%= t('collavre.comments.typo_input_label') %>"
|
|
77
|
+
<% end %>>
|
|
65
78
|
<div class="resize-handle resize-handle-left" data-comments--popup-target="leftHandle"></div>
|
|
66
79
|
<div class="resize-handle resize-handle-right" data-comments--popup-target="rightHandle"></div>
|
|
67
80
|
<div class="comments-popup-header" data-comments--popup-target="header">
|
|
@@ -120,8 +133,10 @@
|
|
|
120
133
|
<div id="comments-list" data-comments--popup-target="list" data-comments--list-target="list"><%= t('app.loading') %></div>
|
|
121
134
|
<div id="typing-indicator-row">
|
|
122
135
|
<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="
|
|
124
|
-
|
|
136
|
+
<div id="typing-scroll-viewport" data-comments--presence-target="scrollRow">
|
|
137
|
+
<div id="channel-chips-container" data-comments--presence-target="channelChips"></div>
|
|
138
|
+
<div id="typing-indicator" data-comments--presence-target="typingIndicator" data-comments--popup-target="typingIndicator" data-stop-agent-text="<%= t('collavre.comments.stop_agent') %>"></div>
|
|
139
|
+
</div>
|
|
125
140
|
</div>
|
|
126
141
|
<form id="new-comment-form" data-comments--popup-target="form" data-comments--form-target="form" style="display:none;">
|
|
127
142
|
<input type="hidden" name="comment[quoted_comment_id]" data-comments--form-target="quotedCommentId" value="" />
|
|
@@ -4,6 +4,7 @@
|
|
|
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
6
|
<input type="hidden" id="inline-content-type" name="creative[content_type_input]" value="html" />
|
|
7
|
+
<input type="hidden" id="inline-markdown-editor" name="creative[markdown_editor]" />
|
|
7
8
|
<input type="hidden" id="inline-markdown-source" name="creative[markdown_source]" />
|
|
8
9
|
<input type="hidden" id="inline-parent-id" name="creative[parent_id]" />
|
|
9
10
|
<input type="hidden" id="inline-before-id" name="before_id" />
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Popup shown after a topic is moved to another creative, offering to re-add
|
|
3
|
+
members who had access on the source creative but not on the target.
|
|
4
|
+
Rendered hidden; populated and shown by topic_move_members_popup.js using the
|
|
5
|
+
`missing_members` payload returned from TopicsController#move.
|
|
6
|
+
%>
|
|
7
|
+
<div id="topic-move-members-modal"
|
|
8
|
+
style="display: none; position: fixed; inset: 0; z-index: 10001; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.3);"
|
|
9
|
+
data-shares-url-template="<%= collavre.creative_creative_shares_path('__CREATIVE_ID__') %>"
|
|
10
|
+
data-adding-text="<%= t('collavre.topics.move.add_members.adding') %>"
|
|
11
|
+
data-added-one="<%= t('collavre.topics.move.add_members.added.one') %>"
|
|
12
|
+
data-added-other="<%= t('collavre.topics.move.add_members.added.other', count: '__COUNT__') %>"
|
|
13
|
+
data-partial-text="<%= t('collavre.topics.move.add_members.partial', added: '__ADDED__', failed: '__FAILED__') %>"
|
|
14
|
+
data-failed-text="<%= t('collavre.topics.move.add_members.failed') %>"
|
|
15
|
+
data-description-template="<%= t('collavre.topics.move.add_members.description', creative: '__CREATIVE_NAME__') %>">
|
|
16
|
+
<div class="popup-box" style="max-width: 460px; width: 90%;">
|
|
17
|
+
<button type="button" id="topic-move-members-close" class="popup-close" aria-label="Close" style="position: absolute; top: 12px; right: 12px;">×</button>
|
|
18
|
+
<h2><%= t('collavre.topics.move.add_members.title') %></h2>
|
|
19
|
+
<p id="topic-move-members-description" style="opacity: 0.75; margin: 0.5em 0 1em;"></p>
|
|
20
|
+
<div id="topic-move-members-message" aria-live="polite"></div>
|
|
21
|
+
<ul id="topic-move-members-list" class="share-grid"></ul>
|
|
22
|
+
<div style="display: flex; gap: 0.5em; justify-content: flex-end; margin-top: 1em;">
|
|
23
|
+
<button type="button" id="topic-move-members-skip" class="btn btn-secondary"><%= t('collavre.topics.move.add_members.skip') %></button>
|
|
24
|
+
<button type="button" id="topic-move-members-add" class="btn btn-primary"><%= t('collavre.topics.move.add_members.add') %></button>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<template id="topic-move-member-row">
|
|
29
|
+
<li class="topic-move-member-row" style="display: flex; align-items: center; gap: 0.5em;">
|
|
30
|
+
<input type="checkbox" class="tmm-checkbox" checked>
|
|
31
|
+
<span class="avatar share-avatar tmm-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; overflow: hidden;"></span>
|
|
32
|
+
<span class="tmm-name"></span>
|
|
33
|
+
<span class="tmm-email" style="opacity: 0.5; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></span>
|
|
34
|
+
<select class="org-chart-permission-select share-modal-permission-select tmm-permission">
|
|
35
|
+
<% %w[admin write feedback read].each do |perm| %>
|
|
36
|
+
<option value="<%= perm %>"><%= t("collavre.creatives.index.permission_#{perm}") %></option>
|
|
37
|
+
<% end %>
|
|
38
|
+
</select>
|
|
39
|
+
<span class="tmm-status" style="width: 16px; text-align: center;"></span>
|
|
40
|
+
</li>
|
|
41
|
+
</template>
|
|
42
|
+
</div>
|
|
@@ -104,6 +104,17 @@
|
|
|
104
104
|
<div data-share-modal-target="container"></div>
|
|
105
105
|
<% end %>
|
|
106
106
|
|
|
107
|
+
<%# The "re-add dropped members" modal must be present wherever a topic can be
|
|
108
|
+
dropped onto another creative โ including the top-level index, where
|
|
109
|
+
@parent_creative/@creative are nil (moving a topic between root-level
|
|
110
|
+
creatives). Gating it on the write check above hid it exactly there, so
|
|
111
|
+
getModal() returned null and the popup silently never appeared. It stays
|
|
112
|
+
hidden until populated, and share creation is permission-checked
|
|
113
|
+
server-side, so rendering it for any authenticated user is safe. %>
|
|
114
|
+
<% if authenticated? %>
|
|
115
|
+
<%= render 'topic_move_members_modal' %>
|
|
116
|
+
<% end %>
|
|
117
|
+
|
|
107
118
|
<%= render 'import_upload_zone' %>
|
|
108
119
|
|
|
109
120
|
<% if can_manage_integrations %>
|
|
@@ -159,6 +170,7 @@
|
|
|
159
170
|
title_effective_origin = @parent_creative.effective_origin(Set.new)
|
|
160
171
|
title_content_type = title_effective_origin.data&.dig("content_type")
|
|
161
172
|
title_markdown_source = title_can_write ? title_effective_origin.data&.dig("markdown_source") : nil
|
|
173
|
+
title_markdown_editor = title_effective_origin.data&.dig("editor")
|
|
162
174
|
%>
|
|
163
175
|
<%= content_tag(
|
|
164
176
|
"creative-tree-row",
|
|
@@ -172,7 +184,8 @@
|
|
|
172
184
|
"data-progress-value": @parent_creative.progress,
|
|
173
185
|
"data-origin-id": @parent_creative.origin_id,
|
|
174
186
|
"data-content-type": title_content_type,
|
|
175
|
-
"data-markdown-source": title_markdown_source
|
|
187
|
+
"data-markdown-source": title_markdown_source,
|
|
188
|
+
"data-markdown-editor": title_markdown_editor
|
|
176
189
|
) %>
|
|
177
190
|
<% else %>
|
|
178
191
|
<%= content_tag(
|
|
@@ -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,
|
|
11
|
+
<div id="slide-content"><%= content_tag(tag_name, embed_youtube_iframe(@creative.effective_description), class: 'creative-content') %></div>
|
|
12
12
|
</div>
|
|
13
13
|
<div id="slide-controls">
|
|
14
14
|
<div id="slide-counter"><%= initial_index + 1 %> / <%= @slide_ids.length %></div>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
<div id="link-creative-modal" class="common-popup" style="display:none;" data-controller="link-creative"
|
|
1
|
+
<div id="link-creative-modal" class="common-popup" style="display:none;" data-controller="link-creative"
|
|
2
|
+
data-link-creative-loading-text="<%= t('collavre.creatives.index.link_loading', default: 'Loadingโฆ') %>"
|
|
3
|
+
data-link-creative-no-results-text="<%= t('collavre.creatives.index.no_search_results', default: 'No creatives match your search.') %>"
|
|
4
|
+
data-link-creative-empty-text="<%= t('collavre.creatives.index.no_sub_creatives', default: 'No sub-creatives found.') %>"
|
|
5
|
+
data-link-creative-expand-text="<%= t('collavre.creatives.index.link_expand', default: 'Expand') %>">
|
|
2
6
|
<button type="button" id="close-link-creative-modal" class="popup-close-btn" data-link-creative-target="close">×</button>
|
|
3
7
|
<h3><%= t('collavre.creatives.index.link', default: 'Link') %></h3>
|
|
4
8
|
<input type="text" id="link-creative-search" class="shared-input-surface" style="width:100%;margin-bottom:0.5em;" placeholder="<%= t('collavre.creatives.index.search_placeholder', default: 'Search creative') %>" data-link-creative-target="input">
|
|
5
|
-
<ul id="link-creative-results" class="common-popup-list" data-popup-list data-link-creative-target="list"></ul>
|
|
9
|
+
<ul id="link-creative-results" class="common-popup-list link-creative-list" data-popup-list data-link-creative-target="list"></ul>
|
|
6
10
|
</div>
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
<%= f.label :timezone, t('collavre.users.timezone') %>
|
|
102
102
|
<%= f.select :timezone, ActiveSupport::TimeZone.all.map { |tz| [tz.to_s, tz.tzinfo.name] } %>
|
|
103
103
|
</div>
|
|
104
|
+
|
|
104
105
|
<%= f.submit t('collavre.users.update_profile') %>
|
|
105
106
|
<% end %>
|
|
106
107
|
|
|
@@ -109,6 +110,8 @@
|
|
|
109
110
|
<br>
|
|
110
111
|
<%= link_to t('collavre.users.webauthn.manage'), collavre.passkeys_user_path(@user) %>
|
|
111
112
|
<br>
|
|
113
|
+
<%= link_to t('collavre.users.typo_correction.manage'), collavre.typo_correction_user_path(@user) %>
|
|
114
|
+
<br>
|
|
112
115
|
<%= link_to t('doorkeeper.my_applications'), main_app.oauth_applications_path %>
|
|
113
116
|
<% if @user.system_admin? %>
|
|
114
117
|
<br>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<h1 class="no-top-margin"><%= t('collavre.users.typo_correction.legend') %></h1>
|
|
2
|
+
|
|
3
|
+
<%= form_with model: @user, url: collavre.user_path(@user), method: :patch, local: true, html: { class: 'profile-form' } do |f| %>
|
|
4
|
+
<p class="text-muted"><%= t('collavre.users.typo_correction.description') %></p>
|
|
5
|
+
|
|
6
|
+
<div class="checkbox-field">
|
|
7
|
+
<%= f.check_box :typo_correction_enabled %>
|
|
8
|
+
<%= f.label :typo_correction_enabled, t('collavre.users.typo_correction.enabled') %>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div>
|
|
12
|
+
<%= f.label :typo_correction_threshold, t('collavre.users.typo_correction.threshold') %>
|
|
13
|
+
<%= f.number_field :typo_correction_threshold, in: 0..100, step: 1, required: true %>
|
|
14
|
+
<small class="text-muted"><%= t('collavre.users.typo_correction.threshold_hint') %></small>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<fieldset>
|
|
18
|
+
<legend><%= t('collavre.users.typo_correction.device_legend') %></legend>
|
|
19
|
+
<div class="checkbox-field">
|
|
20
|
+
<%= f.check_box :typo_correction_on_voice %>
|
|
21
|
+
<%= f.label :typo_correction_on_voice, t('collavre.users.typo_correction.on_voice') %>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="checkbox-field">
|
|
24
|
+
<%= f.check_box :typo_correction_on_soft_keyboard %>
|
|
25
|
+
<%= f.label :typo_correction_on_soft_keyboard, t('collavre.users.typo_correction.on_soft_keyboard') %>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="checkbox-field">
|
|
28
|
+
<%= f.check_box :typo_correction_on_physical_keyboard %>
|
|
29
|
+
<%= f.label :typo_correction_on_physical_keyboard, t('collavre.users.typo_correction.on_physical_keyboard') %>
|
|
30
|
+
</div>
|
|
31
|
+
</fieldset>
|
|
32
|
+
|
|
33
|
+
<fieldset>
|
|
34
|
+
<legend><%= t('collavre.users.typo_correction.location_legend') %></legend>
|
|
35
|
+
<div class="checkbox-field">
|
|
36
|
+
<%= f.check_box :typo_correction_in_chat %>
|
|
37
|
+
<%= f.label :typo_correction_in_chat, t('collavre.users.typo_correction.in_chat') %>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="checkbox-field">
|
|
40
|
+
<%= f.check_box :typo_correction_in_editor %>
|
|
41
|
+
<%= f.label :typo_correction_in_editor, t('collavre.users.typo_correction.in_editor') %>
|
|
42
|
+
</div>
|
|
43
|
+
</fieldset>
|
|
44
|
+
|
|
45
|
+
<%= f.submit t('collavre.users.update') %>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<div style="margin-top: 1rem;">
|
|
49
|
+
<%= link_to t('collavre.users.back'), collavre.user_path(@user) %>
|
|
50
|
+
</div>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<%= stylesheet_link_tag "collavre/design_tokens" %>
|
|
11
11
|
<%= stylesheet_link_tag "collavre/dark_mode" %>
|
|
12
12
|
<%= stylesheet_link_tag 'collavre/slide_view', media: 'all' %>
|
|
13
|
+
<%= stylesheet_link_tag 'collavre/tables', media: 'all' %>
|
|
13
14
|
<%= javascript_include_tag 'actioncable', defer: true %>
|
|
14
15
|
<%= javascript_include_tag 'slide_view', defer: true %>
|
|
15
16
|
<%= render 'collavre/shared/custom_theme_style' %>
|