collavre 0.24.0 → 0.25.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 +0 -1
- data/app/assets/stylesheets/collavre/comments_popup.css +165 -99
- data/app/assets/stylesheets/collavre/creatives.css +346 -37
- data/app/assets/stylesheets/collavre/desktop_setup.css +246 -0
- data/app/assets/stylesheets/collavre/gnb.css +64 -3
- data/app/assets/stylesheets/collavre/landing.css +108 -3
- data/app/assets/stylesheets/collavre/mention_menu.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +14 -0
- data/app/assets/stylesheets/collavre/print.css +0 -1
- data/app/assets/stylesheets/collavre/search_popup.css +1 -1
- data/app/assets/stylesheets/collavre/secret_fields.css +3 -0
- data/app/assets/stylesheets/collavre/workspace.css +307 -0
- data/app/channels/collavre/agent_channel.rb +14 -21
- data/app/channels/collavre/comments_presence_channel.rb +181 -21
- data/app/channels/collavre/inbox_badge_channel.rb +7 -1
- data/app/components/collavre/inbox/badge_component.rb +6 -13
- data/app/components/collavre/progress_filter_component.html.erb +2 -2
- data/app/components/collavre/progress_filter_component.rb +20 -0
- data/app/controllers/collavre/admin/settings_controller.rb +4 -1
- data/app/controllers/collavre/agent_connections_controller.rb +165 -0
- data/app/controllers/collavre/agent_gateways_controller.rb +132 -0
- data/app/controllers/collavre/agent_provisioning_controller.rb +79 -0
- data/app/controllers/collavre/api/v1/agents_controller.rb +45 -97
- data/app/controllers/collavre/application_controller.rb +13 -2
- data/app/controllers/collavre/comment_read_pointers_controller.rb +69 -40
- data/app/controllers/collavre/comments_controller.rb +45 -131
- data/app/controllers/collavre/concerns/shareable.rb +9 -3
- data/app/controllers/collavre/creatives_controller.rb +144 -11
- data/app/controllers/collavre/desktop_gateway_registrations_controller.rb +173 -0
- data/app/controllers/collavre/desktop_setup_controller.rb +119 -0
- data/app/controllers/collavre/desktop_sidecar_health_controller.rb +35 -0
- data/app/controllers/collavre/features_controller.rb +57 -0
- data/app/controllers/collavre/invites_controller.rb +12 -1
- data/app/controllers/collavre/llm_models_controller.rb +8 -0
- data/app/controllers/collavre/notices_controller.rb +20 -0
- data/app/controllers/collavre/tasks_controller.rb +40 -6
- data/app/controllers/collavre/topics_controller.rb +130 -53
- data/app/controllers/collavre/user_creative_preferences_controller.rb +214 -30
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +15 -3
- data/app/controllers/concerns/collavre/comments/conversion.rb +1 -1
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +118 -8
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +1 -15
- data/app/controllers/concerns/collavre/users_controller/registration.rb +2 -2
- data/app/errors/collavre/turn_deadline_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +38 -0
- data/app/helpers/collavre/creatives_helper.rb +56 -22
- data/app/helpers/collavre/navigation_helper.rb +12 -0
- data/app/javascript/collavre.js +1 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +30 -21
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +32 -2
- data/app/javascript/components/__tests__/creative_tree_row_progress_toggle.test.js +185 -0
- data/app/javascript/components/creative_tree_row.js +62 -22
- data/app/javascript/controllers/__tests__/agent_connection_controller.test.js +195 -0
- data/app/javascript/controllers/__tests__/desktop_proxy_setup_controller.test.js +96 -0
- data/app/javascript/controllers/__tests__/index.test.js +42 -0
- data/app/javascript/controllers/__tests__/last_visited_creative_controller.test.js +103 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +253 -2
- data/app/javascript/controllers/__tests__/progress_filter_controller.test.js +134 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +310 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +111 -0
- data/app/javascript/controllers/__tests__/workspace_tree_controller.test.js +1011 -0
- data/app/javascript/controllers/agent_connection_controller.js +357 -0
- data/app/javascript/controllers/agent_vendor_controller.js +23 -0
- data/app/javascript/controllers/comment_controller.js +3 -1
- data/app/javascript/controllers/comments/__tests__/feature_cards_controller.test.js +223 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_default_placeholder.test.js +83 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_draft.test.js +2069 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_ime_send.test.js +324 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +28 -2
- data/app/javascript/controllers/comments/__tests__/form_controller_search_submit.test.js +106 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft.test.js +134 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_creative_switch.test.js +166 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_failure.test.js +183 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_deep_link_topic.test.js +467 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_docked.test.js +115 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_prev_message.test.js +245 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_read_pointer.test.js +276 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_archived.test.js +221 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_search.test.js +104 -0
- data/app/javascript/controllers/comments/__tests__/placeholder_controller.test.js +79 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +754 -1
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +717 -15
- data/app/javascript/controllers/comments/__tests__/prev_message_navigator.test.js +129 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_action_errors.test.js +184 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_archived.test.js +879 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_container.test.js +154 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_scroll.test.js +92 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +130 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_docked.test.js +32 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_last_topic_echo.test.js +1938 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_primary_agent.test.js +256 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_scroll.test.js +182 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_selection_race.test.js +981 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +313 -0
- data/app/javascript/controllers/comments/feature_cards_controller.js +83 -0
- data/app/javascript/controllers/comments/form_controller.js +795 -14
- data/app/javascript/controllers/comments/list_controller.js +338 -52
- data/app/javascript/controllers/comments/placeholder_controller.js +30 -0
- data/app/javascript/controllers/comments/popup_controller.js +425 -83
- data/app/javascript/controllers/comments/presence_controller.js +317 -55
- data/app/javascript/controllers/comments/prev_message_navigator.js +94 -0
- data/app/javascript/controllers/comments/topics_controller.js +1772 -100
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/select_mode_controller.test.js +139 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +605 -0
- data/app/javascript/controllers/creatives/__tests__/write_access_request_controller.test.js +125 -0
- data/app/javascript/controllers/creatives/select_mode_controller.js +7 -2
- data/app/javascript/controllers/creatives/tree_controller.js +140 -31
- data/app/javascript/controllers/creatives/write_access_request_controller.js +58 -0
- data/app/javascript/controllers/desktop_proxy_setup_controller.js +51 -0
- data/app/javascript/controllers/gateway_check_controller.js +36 -0
- data/app/javascript/controllers/index.js +25 -1
- data/app/javascript/controllers/last_visited_creative_controller.js +58 -0
- data/app/javascript/controllers/link_creative_controller.js +89 -18
- data/app/javascript/controllers/progress_filter_controller.js +12 -22
- data/app/javascript/controllers/search_popup_controller.js +64 -46
- data/app/javascript/controllers/share_user_search_controller.js +2 -2
- data/app/javascript/controllers/topic_list_controller.js +44 -7
- data/app/javascript/controllers/workspace_tree_controller.js +662 -0
- data/app/javascript/creatives/__tests__/tree_renderer.test.js +80 -0
- data/app/javascript/creatives/drag_drop/__tests__/drop_completion_empty_state.test.js +98 -0
- data/app/javascript/creatives/drag_drop/__tests__/topic_move_released_agent.test.js +125 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +12 -0
- data/app/javascript/creatives/tree_renderer.js +65 -22
- data/app/javascript/lib/__tests__/chat_drafts.test.js +1458 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +8 -3
- data/app/javascript/lib/__tests__/common_popup_placement.test.js +374 -0
- data/app/javascript/lib/__tests__/common_popup_reshow.test.js +131 -0
- data/app/javascript/lib/__tests__/creative_link_prefetch.test.js +46 -0
- data/app/javascript/lib/__tests__/last_visited_creative.test.js +87 -0
- data/app/javascript/lib/__tests__/skill_html_entities.test.js +23 -0
- data/app/javascript/lib/__tests__/turbo_stream_actions.test.js +190 -0
- data/app/javascript/lib/api/__tests__/creatives.test.js +68 -0
- data/app/javascript/lib/api/__tests__/csrf_fetch.test.js +27 -0
- data/app/javascript/lib/api/__tests__/topics_save_last_topic.test.js +112 -0
- data/app/javascript/lib/api/creatives.js +32 -13
- data/app/javascript/lib/api/csrf_fetch.js +6 -4
- data/app/javascript/lib/api/topics.js +63 -5
- data/app/javascript/lib/chat_drafts.js +786 -0
- data/app/javascript/lib/common_popup.js +164 -25
- data/app/javascript/lib/creative_link_prefetch.js +27 -0
- data/app/javascript/lib/last_visited_creative.js +97 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_node.test.js +162 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_trigger.test.js +133 -0
- data/app/javascript/lib/lexical/__tests__/link_toolbar.test.js +62 -0
- data/app/javascript/lib/lexical/creative_link_node.js +101 -0
- data/app/javascript/lib/lexical/creative_link_trigger.js +98 -0
- data/app/javascript/lib/lexical/link_toolbar.js +23 -0
- data/app/javascript/lib/turbo_stream_actions.js +23 -16
- data/app/javascript/lib/utils/__tests__/filter_navigation.test.js +210 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +44 -0
- data/app/javascript/lib/utils/filter_navigation.js +100 -0
- data/app/javascript/modules/__tests__/command_menu_creative_picker.test.js +62 -0
- data/app/javascript/modules/__tests__/command_menu_typeahead.test.js +267 -0
- data/app/javascript/modules/__tests__/creative_link_picker.test.js +85 -0
- data/app/javascript/modules/__tests__/creative_row_editor_archive.test.js +391 -0
- data/app/javascript/modules/__tests__/creative_row_editor_delegated_clicks.test.js +158 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_recovery.test.js +519 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_removal.test.js +346 -0
- data/app/javascript/modules/__tests__/creative_row_editor_session.test.js +144 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +30 -0
- data/app/javascript/modules/__tests__/creative_tree_empty_state.test.js +186 -0
- data/app/javascript/modules/__tests__/dom_listener_registry.test.js +67 -0
- data/app/javascript/modules/__tests__/support/inline_editor_dom.js +159 -0
- data/app/javascript/modules/command_args_form.js +0 -12
- data/app/javascript/modules/command_menu.js +80 -38
- data/app/javascript/modules/creative_link_picker.js +56 -0
- data/app/javascript/modules/creative_row_editor.js +446 -243
- data/app/javascript/modules/creative_row_editor_delegated_clicks.js +87 -0
- data/app/javascript/modules/creative_tree_dom.js +4 -0
- data/app/javascript/modules/creative_tree_empty_state.js +80 -0
- data/app/javascript/modules/dom_listener_registry.js +23 -0
- data/app/javascript/modules/mention_menu.js +2 -3
- data/app/javascript/utils/__tests__/caret_anchor.test.js +75 -0
- data/app/javascript/utils/__tests__/chevron_icons.test.js +15 -0
- data/app/javascript/utils/caret_position.js +10 -0
- data/app/javascript/utils/chevron_icons.js +7 -0
- data/app/jobs/collavre/ai_agent_job.rb +262 -72
- data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +11 -36
- data/app/jobs/collavre/comment_badges_broadcast_job.rb +28 -0
- data/app/jobs/collavre/comment_link_preview_job.rb +33 -0
- data/app/jobs/collavre/comment_notification_job.rb +14 -0
- data/app/jobs/collavre/comment_push_delivery_sweep_job.rb +17 -0
- data/app/jobs/collavre/compress_job.rb +33 -22
- data/app/jobs/collavre/creative_broadcast_job.rb +161 -48
- data/app/jobs/collavre/cron_action_job.rb +36 -18
- data/app/jobs/collavre/drop_trigger_job.rb +23 -9
- data/app/jobs/collavre/merge_comments_job.rb +33 -16
- data/app/jobs/collavre/push_notification_job.rb +62 -18
- data/app/jobs/collavre/restore_dropped_dispatches_job.rb +17 -0
- data/app/jobs/collavre/stuck_detector_job.rb +1 -1
- data/app/jobs/collavre/touch_creative_subtree_job.rb +14 -0
- data/app/models/collavre/agent_gateway.rb +200 -0
- data/app/models/collavre/agent_subscription.rb +4 -4
- data/app/models/collavre/agent_workspace.rb +210 -0
- data/app/models/collavre/comment/broadcastable.rb +22 -52
- data/app/models/collavre/comment/notifiable.rb +154 -77
- data/app/models/collavre/comment/topic_membership.rb +41 -0
- data/app/models/collavre/comment.rb +336 -31
- data/app/models/collavre/comment_notification_delivery.rb +61 -0
- data/app/models/collavre/comment_presence_store/cache_batches.rb +64 -0
- data/app/models/collavre/comment_presence_store.rb +272 -12
- data/app/models/collavre/comment_read_pointer.rb +9 -15
- data/app/models/collavre/creative/describable.rb +8 -2
- data/app/models/collavre/creative/permissible.rb +11 -0
- data/app/models/collavre/creative.rb +29 -4
- data/app/models/collavre/creative_share.rb +109 -4
- data/app/models/collavre/current.rb +1 -0
- data/app/models/collavre/llm_model.rb +62 -0
- data/app/models/collavre/system_setting.rb +12 -1
- data/app/models/collavre/task.rb +121 -2
- data/app/models/collavre/topic.rb +51 -0
- data/app/models/collavre/user.rb +101 -41
- data/app/models/collavre/user_creative_preference.rb +3 -1
- data/app/services/collavre/agent_provisioning/archive.rb +108 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +16 -3
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +63 -6
- data/app/services/collavre/ai_agent/approval_handler.rb +12 -4
- data/app/services/collavre/ai_agent/claude_channel_adapter.rb +10 -5
- data/app/services/collavre/ai_agent/merged_trigger_comments.rb +164 -0
- data/app/services/collavre/ai_agent/message_builder.rb +103 -16
- data/app/services/collavre/ai_agent/session_provisioner.rb +3 -7
- data/app/services/collavre/ai_agent/task_claim_service.rb +29 -15
- data/app/services/collavre/ai_agent/task_reply_service.rb +67 -0
- data/app/services/collavre/ai_agent_service.rb +108 -20
- data/app/services/collavre/ai_client.rb +211 -21
- data/app/services/collavre/cli_proxy/client.rb +147 -0
- data/app/services/collavre/cli_proxy/endpoint_policy.rb +81 -0
- data/app/services/collavre/cli_proxy/identity.rb +47 -0
- data/app/services/collavre/cli_proxy/safe_net_http_adapter.rb +19 -0
- data/app/services/collavre/command_menu_service.rb +0 -10
- data/app/services/collavre/comment_move_service.rb +96 -7
- data/app/services/collavre/comment_snapshot_restore_service.rb +11 -5
- data/app/services/collavre/comments/action_executor.rb +48 -20
- data/app/services/collavre/comments/command_processor.rb +0 -1
- data/app/services/collavre/comments/list_response.rb +133 -0
- data/app/services/collavre/comments/read_pointer_writer.rb +210 -0
- data/app/services/collavre/comments/read_receipt_broadcaster.rb +157 -0
- data/app/services/collavre/comments/read_receipt_index.rb +152 -0
- data/app/services/collavre/comments/topic_command.rb +72 -1
- data/app/services/collavre/comments/topic_mutation.rb +20 -0
- data/app/services/collavre/creatives/breadcrumb_resolver.rb +1 -1
- data/app/services/collavre/creatives/comment_badge_index.rb +299 -27
- data/app/services/collavre/creatives/reorderer.rb +15 -3
- data/app/services/collavre/creatives/tree_builder.rb +1 -1
- data/app/services/collavre/creatives/tree_formatter.rb +5 -6
- data/app/services/collavre/creatives/workspace_path_resolver.rb +50 -0
- data/app/services/collavre/creatives/workspace_tree_builder.rb +92 -0
- data/app/services/collavre/crons/recurring_topic_tasks.rb +23 -0
- data/app/services/collavre/home_page_path.rb +55 -0
- data/app/services/collavre/html_text.rb +64 -0
- data/app/services/collavre/http_client.rb +11 -2
- data/app/services/collavre/inbox_reply_service.rb +1 -1
- data/app/services/collavre/markdown_converter.rb +13 -1
- data/app/services/collavre/orchestration/agent_orchestrator.rb +776 -81
- data/app/services/collavre/orchestration/arbiter.rb +48 -5
- data/app/services/collavre/orchestration/deferred_trigger_scope.rb +53 -0
- data/app/services/collavre/orchestration/delivery_record.rb +792 -0
- data/app/services/collavre/orchestration/loop_breaker.rb +10 -4
- data/app/services/collavre/orchestration/matcher.rb +132 -17
- data/app/services/collavre/orchestration/policy_resolver.rb +58 -10
- data/app/services/collavre/orchestration/scheduler.rb +10 -4
- data/app/services/collavre/orchestration/scheduling_hooks.rb +10 -0
- data/app/services/collavre/orchestration/selection.rb +43 -0
- data/app/services/collavre/orchestration/stuck_detector.rb +25 -112
- data/app/services/collavre/orchestration/task_coalescer.rb +355 -0
- data/app/services/collavre/orchestration/topic_slot.rb +73 -0
- data/app/services/collavre/system_events/context_builder.rb +60 -11
- data/app/services/collavre/system_events/dispatcher.rb +58 -12
- data/app/services/collavre/system_events/envelope.rb +73 -0
- data/app/services/collavre/system_events/vocabulary.rb +45 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +1 -1
- data/app/services/collavre/tools/cron_create_service.rb +32 -18
- data/app/services/collavre/tools/id_list.rb +38 -0
- data/app/services/collavre/tools/topic_authorizer.rb +61 -7
- data/app/services/collavre/tools/topic_branch_service.rb +85 -0
- data/app/services/collavre/tools/topic_create_service.rb +117 -0
- data/app/services/collavre/tools/topic_list_service.rb +101 -0
- data/app/services/collavre/tools/topic_message_create_service.rb +192 -0
- data/app/services/collavre/tools/topic_messages_service.rb +346 -0
- data/app/services/collavre/tools/topic_move_service.rb +108 -0
- data/app/services/collavre/tools/topic_selection.rb +59 -0
- data/app/services/collavre/tools/topic_update_service.rb +165 -0
- data/app/services/collavre/topic_branch_service.rb +39 -10
- data/app/services/collavre/topics/agent_resolver.rb +87 -0
- data/app/services/collavre/topics/char_budget.rb +70 -0
- data/app/services/collavre/topics/last_topic_preference_payload.rb +18 -0
- data/app/services/collavre/topics/message_page.rb +380 -0
- data/app/services/collavre/topics/message_page_markdown.rb +102 -0
- data/app/services/collavre/topics/message_scope.rb +61 -0
- data/app/services/collavre/topics/message_stats.rb +92 -0
- data/app/services/collavre/topics/move_blocker.rb +47 -0
- data/app/services/collavre/topics/next_name.rb +35 -0
- data/app/services/collavre/topics/pin_rejection.rb +25 -0
- data/app/services/collavre/topics/read_pointer_relocator.rb +71 -0
- data/app/services/collavre/topics/reserved_name.rb +18 -0
- data/app/services/collavre/topics/serializer.rb +92 -0
- data/app/services/collavre/topics/topic_destroy.rb +30 -0
- data/app/services/collavre/topics/topic_move.rb +80 -0
- data/app/services/collavre/topics/topic_move_effects.rb +35 -0
- data/app/services/collavre/topics/topic_mutation.rb +27 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +5 -8
- data/app/views/collavre/admin/settings/_system_tab.html.erb +7 -0
- data/app/views/collavre/agent_connections/show.html.erb +77 -0
- data/app/views/collavre/agent_gateways/_form.html.erb +71 -0
- data/app/views/collavre/agent_gateways/edit.html.erb +2 -0
- data/app/views/collavre/agent_gateways/index.html.erb +54 -0
- data/app/views/collavre/agent_gateways/new.html.erb +2 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -17
- data/app/views/collavre/comments/_empty_state.html.erb +33 -0
- data/app/views/collavre/comments/_empty_state_minimal.html.erb +7 -0
- data/app/views/collavre/comments/_feature_card.html.erb +35 -0
- data/app/views/collavre/comments/_list.html.erb +3 -2
- data/app/views/collavre/comments/_no_search_results.html.erb +3 -0
- data/app/views/collavre/creatives/_add_button.html.erb +17 -19
- data/app/views/collavre/creatives/_empty_state.html.erb +109 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +5 -5
- data/app/views/collavre/creatives/_tree_loading_placeholder.html.erb +16 -0
- data/app/views/collavre/creatives/_workspace_tree.html.erb +33 -0
- data/app/views/collavre/creatives/index.html.erb +79 -18
- data/app/views/collavre/desktop_setup/show.html.erb +114 -0
- data/app/views/collavre/features/index.html.erb +42 -0
- data/app/views/collavre/features/show.html.erb +52 -0
- data/app/views/collavre/invitation_mailer/invite.html.erb +1 -1
- data/app/views/collavre/invitation_mailer/invite.text.erb +1 -1
- data/app/views/collavre/invites/show.html.erb +14 -1
- data/app/views/collavre/landing/show.html.erb +5 -1
- data/app/views/collavre/shared/_link_creative_modal.html.erb +4 -1
- data/app/views/collavre/shared/navigation/_help_button.html.erb +3 -1
- data/app/views/collavre/shared/navigation/_panels.html.erb +1 -8
- data/app/views/collavre/shared/navigation/_search_form.html.erb +13 -2
- data/app/views/collavre/user_themes/index.html.erb +1 -1
- data/app/views/collavre/users/_llm_fields.html.erb +65 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +1 -1
- data/app/views/collavre/users/edit_ai.html.erb +33 -31
- data/app/views/collavre/users/new.html.erb +4 -2
- data/app/views/collavre/users/new_ai.html.erb +14 -32
- data/app/views/collavre/users/show.html.erb +13 -1
- data/app/views/layouts/collavre/landing.html.erb +9 -4
- data/config/initializers/ruby_llm_openai_reasoning_content.rb +77 -0
- data/config/locales/admin.en.yml +3 -1
- data/config/locales/admin.ko.yml +3 -1
- data/config/locales/agent_gateway.en.yml +217 -0
- data/config/locales/agent_gateway.ko.yml +217 -0
- data/config/locales/ai_agent.en.yml +0 -1
- data/config/locales/ai_agent.ko.yml +0 -1
- data/config/locales/comments.en.yml +72 -30
- data/config/locales/comments.ko.yml +72 -30
- data/config/locales/creatives.en.yml +32 -1
- data/config/locales/creatives.ko.yml +32 -1
- data/config/locales/features.en.yml +140 -0
- data/config/locales/features.ko.yml +140 -0
- data/config/locales/invites.en.yml +3 -0
- data/config/locales/invites.ko.yml +3 -0
- data/config/locales/landing.en.yml +2 -1
- data/config/locales/landing.ko.yml +2 -1
- data/config/locales/tools.en.yml +20 -0
- data/config/locales/tools.ko.yml +20 -0
- data/config/locales/users.en.yml +15 -14
- data/config/locales/users.ko.yml +14 -14
- data/config/routes.rb +45 -19
- data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +9 -1
- data/db/migrate/20260724000000_add_creative_id_index_to_comments.rb +34 -0
- data/db/migrate/20260724000001_add_creative_private_id_index_to_comments.rb +50 -0
- data/db/migrate/20260724090000_add_async_comment_notification_state.rb +64 -0
- data/db/migrate/20260727000000_add_waiting_notice_scope_to_comments.rb +30 -0
- data/db/migrate/20260727010000_add_waiting_notice_scope_to_tasks.rb +26 -0
- data/db/migrate/20260805000000_add_creative_workspace_enabled_to_users.rb +5 -0
- data/db/migrate/20260807050000_add_dismissed_notices_to_users.rb +5 -0
- data/db/migrate/20260809000000_change_creative_workspace_enabled_default_to_true.rb +18 -0
- data/db/migrate/20260809000001_create_llm_models.rb +64 -0
- data/db/migrate/20260809000002_create_agent_gateways_and_workspaces.rb +35 -0
- data/db/migrate/20260809000003_hash_agent_workspace_callback_tokens.rb +44 -0
- data/db/migrate/20260809000004_encrypt_agent_workspace_manifest_tokens.rb +59 -0
- data/db/migrate/20260810000000_split_agent_workspace_identity_axes.rb +58 -0
- data/db/migrate/20260810000001_seed_codex_custom_proxy_llm_models.rb +28 -0
- data/db/migrate/20260810010000_add_last_visited_creative_to_users.rb +8 -0
- data/db/migrate/20260810020000_add_last_visited_creative_at_to_users.rb +5 -0
- data/db/migrate/20260810030000_add_last_visited_creative_navigation_order_to_users.rb +6 -0
- data/db/migrate/20260810060000_add_last_visited_creative_issued_sequence_to_users.rb +5 -0
- data/db/migrate/20260810100000_add_desktop_managed_to_agent_gateways.rb +40 -0
- data/db/migrate/20260810100001_add_desktop_preset_adapter_to_users.rb +38 -0
- data/db/migrate/20260811000000_make_agent_gateway_completion_key_nullable.rb +7 -0
- data/db/migrate/20260812000000_remove_workflow_columns_from_tasks.rb +101 -0
- data/db/migrate/20260812000001_remove_android_devices.rb +24 -0
- data/db/migrate/20260812010000_drop_inbox_items.rb +22 -0
- data/db/migrate/20260813000000_clear_claude_channel_presence_routing_expressions.rb +16 -0
- data/db/migrate/20260813000001_add_last_topic_revision_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000002_add_last_topic_save_order_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260813000003_add_last_topic_save_sequences_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000004_add_last_topic_save_fences_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260814000000_add_topic_to_comment_read_pointers.rb +79 -0
- data/db/migrate/20260814000001_add_creative_topic_private_id_index_to_comments.rb +37 -0
- data/db/migrate/20260816000000_add_watermark_index_to_comment_read_pointers.rb +27 -0
- data/db/migrate/20260816000001_add_private_recipient_topic_index_to_comments.rb +48 -0
- data/db/migrate/20260820000000_add_topic_assigned_at_to_comments.rb +13 -0
- data/db/seeds.rb +1 -51
- data/lib/collavre/engine.rb +108 -1
- data/lib/collavre/feature_card_registry.rb +168 -0
- data/lib/collavre/hashed_access_token_lookup.rb +22 -0
- data/lib/collavre/sensitive_request_silencer.rb +23 -0
- data/lib/collavre/user_extensions.rb +0 -2
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +1 -0
- data/skills/collavre/SKILL.md +165 -0
- data/skills/collavre/package.json +8 -0
- data/skills/collavre/references/tool-reference.md +347 -0
- data/skills/collavre/scripts/collavre +634 -0
- data/skills/collavre/scripts/html_entities.js +24 -0
- metadata +228 -30
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +0 -224
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +0 -95
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +0 -31
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +0 -25
- data/app/controllers/collavre/inbox_items_controller.rb +0 -70
- data/app/controllers/collavre/typo_corrections_controller.rb +0 -39
- data/app/javascript/lib/__tests__/typo_correction.test.js +0 -192
- data/app/javascript/lib/typo_correction.js +0 -146
- data/app/javascript/modules/__tests__/typo_corrector.test.js +0 -365
- data/app/javascript/modules/creative_guide.js +0 -67
- data/app/javascript/modules/typo_corrector.js +0 -534
- data/app/jobs/collavre/inbox_summary_job.rb +0 -24
- data/app/mailers/collavre/inbox_mailer.rb +0 -19
- data/app/models/collavre/inbox_item.rb +0 -89
- data/app/services/collavre/comments/concerns/workflow_support.rb +0 -115
- data/app/services/collavre/comments/work_command.rb +0 -161
- data/app/services/collavre/comments/workflow_executor.rb +0 -276
- data/app/services/collavre/gemini_parent_recommender.rb +0 -77
- data/app/services/collavre/mobile/event_summarizer.rb +0 -40
- data/app/services/collavre/typo_corrector.rb +0 -188
- data/app/views/collavre/inbox_items/_item.html.erb +0 -18
- data/app/views/collavre/inbox_items/_items.html.erb +0 -3
- data/app/views/collavre/inbox_items/_list.html.erb +0 -7
- data/app/views/collavre/inbox_items/index.html.erb +0 -1
- data/app/views/collavre/inbox_mailer/daily_summary.html.erb +0 -11
- data/app/views/collavre/inbox_mailer/daily_summary.text.erb +0 -8
- data/app/views/collavre/users/typo_correction.html.erb +0 -50
- data/config/locales/mobile.en.yml +0 -16
- data/config/locales/mobile.ko.yml +0 -16
|
@@ -1,534 +0,0 @@
|
|
|
1
|
-
// Inline typo correction for the chat composer (Phase 1).
|
|
2
|
-
//
|
|
3
|
-
// Wires the chat textarea to the server endpoint and paints a *volatile*
|
|
4
|
-
// highlight overlay: a mirror "backdrop" div sitting exactly behind the textarea
|
|
5
|
-
// with <mark> spans over each edit span. The textarea value stays the single
|
|
6
|
-
// source of truth — the backdrop is aria-hidden and never serialized, so
|
|
7
|
-
// markdown-canonical storage never sees a highlight.
|
|
8
|
-
//
|
|
9
|
-
// Two highlight states (shape + colour, for colour-blind safety):
|
|
10
|
-
// - auto-applied (confidence >= threshold): faint straight underline
|
|
11
|
-
// - candidate (below threshold): wavy dotted underline (needs decision)
|
|
12
|
-
//
|
|
13
|
-
// Clicking a highlight opens a creatable combobox (reusing CommonPopup for
|
|
14
|
-
// positioning + list + keyboard nav) pre-filled with the word currently in the
|
|
15
|
-
// document, so Enter = keep. Typing adds the typed word as an always-present,
|
|
16
|
-
// auto-selected custom option.
|
|
17
|
-
|
|
18
|
-
import CommonPopup from '../lib/common_popup'
|
|
19
|
-
import csrfFetch from '../lib/api/csrf_fetch'
|
|
20
|
-
import {
|
|
21
|
-
detectDevice,
|
|
22
|
-
shouldRun,
|
|
23
|
-
buildCandidateList,
|
|
24
|
-
anchorEdits,
|
|
25
|
-
applyEditAt,
|
|
26
|
-
shiftEditsAfter,
|
|
27
|
-
partitionByThreshold,
|
|
28
|
-
} from '../lib/typo_correction'
|
|
29
|
-
|
|
30
|
-
const DEBOUNCE_MS = 600
|
|
31
|
-
const PRINTABLE_KEYDOWN_TTL_MS = 50
|
|
32
|
-
|
|
33
|
-
let initialized = false
|
|
34
|
-
|
|
35
|
-
// Mirror the textarea's box metrics onto the backdrop so highlight spans land on
|
|
36
|
-
// the exact glyphs. Only the properties that affect text layout are copied.
|
|
37
|
-
const MIRRORED_STYLES = [
|
|
38
|
-
'boxSizing', 'width', 'height', 'paddingTop', 'paddingRight', 'paddingBottom',
|
|
39
|
-
'paddingLeft', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth',
|
|
40
|
-
'borderLeftWidth', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle',
|
|
41
|
-
'lineHeight', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent',
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
// Escape by round-tripping through textContent: the browser never reinterprets
|
|
45
|
-
// it as markup, and it's the canonical DOM-text sanitizer (recognized as a
|
|
46
|
-
// barrier by static XSS analysis) for the values we splice into the backdrop and
|
|
47
|
-
// popup HTML — both of which carry user-typed / model-suggested text.
|
|
48
|
-
function escapeHtml(value) {
|
|
49
|
-
const el = document.createElement('div')
|
|
50
|
-
el.textContent = String(value)
|
|
51
|
-
return el.innerHTML
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class TypoCorrector {
|
|
55
|
-
constructor(textarea, { settings, location = 'chat', endpoint = '/typo_corrections', getVoiceActive = () => false, labels = {} } = {}) {
|
|
56
|
-
this.textarea = textarea
|
|
57
|
-
this.settings = settings
|
|
58
|
-
this.location = location
|
|
59
|
-
this.endpoint = endpoint
|
|
60
|
-
this.getVoiceActive = getVoiceActive
|
|
61
|
-
this.labels = {
|
|
62
|
-
keep: labels.keep || 'keep',
|
|
63
|
-
custom: labels.custom || 'custom',
|
|
64
|
-
inputLabel: labels.inputLabel || 'correction',
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.edits = [] // anchored edits currently painted: {start,end,original,suggestion,confidence,state}
|
|
68
|
-
this.lastPrintableKeydownAt = null
|
|
69
|
-
this.lastInputAt = null
|
|
70
|
-
this.debounceTimer = null
|
|
71
|
-
this.requestSeq = 0
|
|
72
|
-
|
|
73
|
-
this._buildBackdrop()
|
|
74
|
-
this._bind()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
_buildBackdrop() {
|
|
78
|
-
const ta = this.textarea
|
|
79
|
-
const wrap = document.createElement('div')
|
|
80
|
-
wrap.className = 'typo-input-wrap'
|
|
81
|
-
ta.parentNode.insertBefore(wrap, ta)
|
|
82
|
-
|
|
83
|
-
this.backdrop = document.createElement('div')
|
|
84
|
-
this.backdrop.className = 'typo-backdrop'
|
|
85
|
-
this.backdrop.setAttribute('aria-hidden', 'true')
|
|
86
|
-
this.highlightLayer = document.createElement('div')
|
|
87
|
-
this.highlightLayer.className = 'typo-highlights'
|
|
88
|
-
this.backdrop.appendChild(this.highlightLayer)
|
|
89
|
-
|
|
90
|
-
wrap.appendChild(this.backdrop)
|
|
91
|
-
wrap.appendChild(ta)
|
|
92
|
-
this._syncBackdropStyle()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
_syncBackdropStyle() {
|
|
96
|
-
const computed = getComputedStyle(this.textarea)
|
|
97
|
-
MIRRORED_STYLES.forEach((prop) => {
|
|
98
|
-
this.highlightLayer.style[prop] = computed[prop]
|
|
99
|
-
})
|
|
100
|
-
// The textarea's own border is transparent-mirrored via padding offsets.
|
|
101
|
-
this.highlightLayer.style.borderColor = 'transparent'
|
|
102
|
-
this.highlightLayer.style.borderStyle = 'solid'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
_bind() {
|
|
106
|
-
this._onKeydown = (event) => {
|
|
107
|
-
// A printable keydown immediately preceding the input marks a physical key.
|
|
108
|
-
if (event.key && event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
109
|
-
this.lastPrintableKeydownAt = performance.now()
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
this._onInput = () => {
|
|
113
|
-
// Stamp the input time *now*, while the keydown→input relationship is still
|
|
114
|
-
// fresh. Classifying at detect time (after the debounce) would read a
|
|
115
|
-
// ~600ms gap and misread every physical keypress as a soft keyboard,
|
|
116
|
-
// defeating the default-off physical-keyboard gate.
|
|
117
|
-
this.lastInputAt = performance.now()
|
|
118
|
-
this._syncScroll()
|
|
119
|
-
this._repaint() // keep existing highlights aligned with edited text
|
|
120
|
-
// Our own auto-apply dispatches a synthetic `input` so other listeners
|
|
121
|
-
// (auto-resize, draft save, send-button state) update. We must NOT let it
|
|
122
|
-
// re-trigger detection: the corrected text has no typos, so the follow-up
|
|
123
|
-
// would return [] and wipe the just-applied underline/undo affordance
|
|
124
|
-
// before the user can click it. Real keystrokes still schedule detection.
|
|
125
|
-
if (this.suppressNextDetect) {
|
|
126
|
-
this.suppressNextDetect = false
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
this._scheduleDetect()
|
|
130
|
-
}
|
|
131
|
-
this._onScroll = () => this._syncScroll()
|
|
132
|
-
// form.reset() (after a comment sends, or on cancel) empties the textarea
|
|
133
|
-
// WITHOUT firing an `input` event, so our repaint never runs and stale marks
|
|
134
|
-
// would linger over the now-empty composer — still clickable, applying stale
|
|
135
|
-
// offsets to the empty value. The `reset` event fires before the control is
|
|
136
|
-
// cleared, so drop the edits now and repaint next frame once it's empty.
|
|
137
|
-
this._onReset = () => {
|
|
138
|
-
clearTimeout(this.debounceTimer)
|
|
139
|
-
this.requestSeq++ // invalidate any in-flight detection response
|
|
140
|
-
this.edits = []
|
|
141
|
-
requestAnimationFrame(() => this._repaint())
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this.form = this.textarea.form
|
|
145
|
-
this.textarea.addEventListener('keydown', this._onKeydown)
|
|
146
|
-
this.textarea.addEventListener('input', this._onInput)
|
|
147
|
-
this.textarea.addEventListener('scroll', this._onScroll)
|
|
148
|
-
this.form?.addEventListener('reset', this._onReset)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
destroy() {
|
|
152
|
-
clearTimeout(this.debounceTimer)
|
|
153
|
-
this.textarea.removeEventListener('keydown', this._onKeydown)
|
|
154
|
-
this.textarea.removeEventListener('input', this._onInput)
|
|
155
|
-
this.textarea.removeEventListener('scroll', this._onScroll)
|
|
156
|
-
this.form?.removeEventListener('reset', this._onReset)
|
|
157
|
-
// Drop the combobox popup: it lives on document.body, so without this a Turbo
|
|
158
|
-
// page-cache snapshot serializes an orphan popup (stale options, no live
|
|
159
|
-
// instance) and the next corrector appends a duplicate. hide() also detaches
|
|
160
|
-
// CommonPopup's document-level outside-click listeners.
|
|
161
|
-
this.popup?.hide()
|
|
162
|
-
this.popupEl?.remove()
|
|
163
|
-
this.popupEl = null
|
|
164
|
-
this.popupInput = null
|
|
165
|
-
this.popup = null
|
|
166
|
-
// Unwrap the textarea and drop the injected backdrop so a Turbo page-cache
|
|
167
|
-
// snapshot doesn't serialize stale overlay DOM (it would duplicate on the
|
|
168
|
-
// next build) or the bind marker (a restored snapshot keeps typoBound=true
|
|
169
|
-
// but loses the JS listeners, so re-init is skipped and correction stays
|
|
170
|
-
// dead until a full reload).
|
|
171
|
-
const wrap = this.backdrop?.parentNode
|
|
172
|
-
if (wrap && wrap.parentNode) {
|
|
173
|
-
wrap.parentNode.insertBefore(this.textarea, wrap)
|
|
174
|
-
wrap.remove()
|
|
175
|
-
}
|
|
176
|
-
delete this.textarea.dataset.typoBound
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
_syncScroll() {
|
|
180
|
-
this.backdrop.scrollTop = this.textarea.scrollTop
|
|
181
|
-
this.backdrop.scrollLeft = this.textarea.scrollLeft
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
_currentDevice() {
|
|
185
|
-
return detectDevice({
|
|
186
|
-
voiceActive: !!this.getVoiceActive(),
|
|
187
|
-
lastPrintableKeydownAt: this.lastPrintableKeydownAt,
|
|
188
|
-
inputAt: this.lastInputAt,
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
_scheduleDetect() {
|
|
193
|
-
clearTimeout(this.debounceTimer)
|
|
194
|
-
this.debounceTimer = setTimeout(() => this._detect(), DEBOUNCE_MS)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async _detect() {
|
|
198
|
-
const device = this._currentDevice();
|
|
199
|
-
// Reset the printable-keydown marker so a stale value can't mis-classify the
|
|
200
|
-
// next standalone input (e.g. soft-keyboard insert after a physical keypress).
|
|
201
|
-
if (this.lastPrintableKeydownAt != null && performance.now() - this.lastPrintableKeydownAt > PRINTABLE_KEYDOWN_TTL_MS) {
|
|
202
|
-
this.lastPrintableKeydownAt = null
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (!shouldRun(this.settings, { device, location: this.location })) return
|
|
206
|
-
const text = this.textarea.value
|
|
207
|
-
if (!text.trim()) { this._clear(); return }
|
|
208
|
-
|
|
209
|
-
const seq = ++this.requestSeq
|
|
210
|
-
let payload
|
|
211
|
-
try {
|
|
212
|
-
const res = await csrfFetch(this.endpoint, {
|
|
213
|
-
method: 'POST',
|
|
214
|
-
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
215
|
-
body: JSON.stringify({ text, device, location: this.location }),
|
|
216
|
-
})
|
|
217
|
-
if (!res.ok) return
|
|
218
|
-
payload = await res.json()
|
|
219
|
-
} catch {
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
if (seq !== this.requestSeq) return // a newer keystroke superseded this one
|
|
223
|
-
// The text may have changed while the request was in flight; only act if the
|
|
224
|
-
// edits still anchor to the *current* textarea value.
|
|
225
|
-
if (this.textarea.value !== text) return
|
|
226
|
-
|
|
227
|
-
this._applyResult(payload)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
_applyResult({ edits = [], threshold = 80 } = {}) {
|
|
231
|
-
const original = this.textarea.value
|
|
232
|
-
const anchored = anchorEdits(original, edits)
|
|
233
|
-
const { autoApplied, candidates } = partitionByThreshold(anchored, threshold)
|
|
234
|
-
|
|
235
|
-
// This round's edits. `apply` rewrites the word; `keep` leaves the text as-is
|
|
236
|
-
// (a low-confidence candidate awaiting a decision).
|
|
237
|
-
const incoming = [
|
|
238
|
-
...autoApplied.map((e) => ({ ...e, kind: 'apply' })),
|
|
239
|
-
...candidates.map((e) => ({ ...e, kind: 'keep', state: 'candidate', currentValue: e.original })),
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
// Carry forward earlier corrections that are still anchored in the live text
|
|
243
|
-
// and don't collide with this round. The server is stateless and never
|
|
244
|
-
// re-reports an already-corrected word, so without this every detection round
|
|
245
|
-
// would wipe the prior correction's mark and leave only the most recent one
|
|
246
|
-
// clickable. Already-applied survivors sit in the text unchanged, so they're
|
|
247
|
-
// re-emitted in place (kind 'keep'), not re-applied.
|
|
248
|
-
const survivors = this.edits
|
|
249
|
-
.filter((p) => original.slice(p.start, p.end) === p.currentValue)
|
|
250
|
-
.filter((p) => !incoming.some((n) => n.start < p.end && p.start < n.end))
|
|
251
|
-
.map((p) => ({ ...p, kind: 'keep' }))
|
|
252
|
-
|
|
253
|
-
// Walk all edits left-to-right in one pass, rebuilding the text while
|
|
254
|
-
// tracking a running delta. This assigns each tracked edit an *exact* final
|
|
255
|
-
// span, rather than re-searching the corrected word by value afterwards —
|
|
256
|
-
// that search would bind to an earlier identical word (e.g. correcting the
|
|
257
|
-
// second "teh" in "the teh" lands the highlight on the first "the", so undo
|
|
258
|
-
// would rewrite the wrong word).
|
|
259
|
-
const events = [...incoming, ...survivors].sort((a, b) => a.start - b.start)
|
|
260
|
-
|
|
261
|
-
const originalCaret = this.textarea.selectionStart
|
|
262
|
-
let out = ''
|
|
263
|
-
let cursor = 0
|
|
264
|
-
let delta = 0
|
|
265
|
-
let caret = originalCaret
|
|
266
|
-
const tracked = []
|
|
267
|
-
for (const e of events) {
|
|
268
|
-
if (e.start < cursor) continue // overlapping span; skip defensively
|
|
269
|
-
out += original.slice(cursor, e.start)
|
|
270
|
-
const finalStart = e.start + delta
|
|
271
|
-
if (e.kind === 'apply') {
|
|
272
|
-
out += e.suggestion
|
|
273
|
-
const d = e.suggestion.length - (e.end - e.start)
|
|
274
|
-
if (e.start < originalCaret) caret += d
|
|
275
|
-
delta += d
|
|
276
|
-
tracked.push({
|
|
277
|
-
original: e.original, suggestion: e.suggestion, confidence: e.confidence,
|
|
278
|
-
state: 'applied', currentValue: e.suggestion,
|
|
279
|
-
start: finalStart, end: finalStart + e.suggestion.length,
|
|
280
|
-
})
|
|
281
|
-
} else {
|
|
282
|
-
// 'keep': a new candidate or a carried-forward survivor — text unchanged,
|
|
283
|
-
// so re-emit the current slice and preserve its existing state.
|
|
284
|
-
const slice = original.slice(e.start, e.end)
|
|
285
|
-
out += slice
|
|
286
|
-
tracked.push({
|
|
287
|
-
original: e.original, suggestion: e.suggestion, confidence: e.confidence,
|
|
288
|
-
state: e.state, currentValue: e.currentValue,
|
|
289
|
-
start: finalStart, end: finalStart + slice.length,
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
cursor = e.end
|
|
293
|
-
}
|
|
294
|
-
out += original.slice(cursor)
|
|
295
|
-
|
|
296
|
-
this.edits = tracked
|
|
297
|
-
if (out !== original) {
|
|
298
|
-
this.textarea.value = out
|
|
299
|
-
this.textarea.setSelectionRange(caret, caret)
|
|
300
|
-
// Our own mutation: notify other listeners but don't re-run detection (it
|
|
301
|
-
// would return [] on the now-clean text and wipe these fresh highlights).
|
|
302
|
-
this.suppressNextDetect = true
|
|
303
|
-
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
304
|
-
}
|
|
305
|
-
this._repaint()
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
_clear() {
|
|
309
|
-
this.edits = []
|
|
310
|
-
this._repaint()
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Paint the backdrop: plain text with <mark> spans over each edit. Marks carry
|
|
314
|
-
// the state class so CSS renders the two underline styles.
|
|
315
|
-
_repaint() {
|
|
316
|
-
// Drop edits that no longer match the live text (user kept typing).
|
|
317
|
-
const text = this.textarea.value
|
|
318
|
-
this.edits = this.edits.filter((e) => text.slice(e.start, e.end) === e.currentValue)
|
|
319
|
-
|
|
320
|
-
if (this.edits.length === 0) {
|
|
321
|
-
this.highlightLayer.textContent = text
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
const sorted = [...this.edits].sort((a, b) => a.start - b.start)
|
|
325
|
-
let html = ''
|
|
326
|
-
let cursor = 0
|
|
327
|
-
sorted.forEach((edit, i) => {
|
|
328
|
-
if (edit.start < cursor) return // overlapping; skip
|
|
329
|
-
html += escapeHtml(text.slice(cursor, edit.start))
|
|
330
|
-
const cls = edit.state === 'applied' ? 'typo-mark typo-mark-applied' : 'typo-mark typo-mark-candidate'
|
|
331
|
-
html += `<mark class="${cls}" data-typo-index="${i}">${escapeHtml(text.slice(edit.start, edit.end))}</mark>`
|
|
332
|
-
cursor = edit.end
|
|
333
|
-
})
|
|
334
|
-
html += escapeHtml(text.slice(cursor))
|
|
335
|
-
this.highlightLayer.innerHTML = html
|
|
336
|
-
this._wireMarks(sorted)
|
|
337
|
-
this._syncScroll()
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// <mark> has pointer-events:auto (the rest of the backdrop is none), so clicks
|
|
341
|
-
// land on a highlighted word and open the combobox there.
|
|
342
|
-
_wireMarks(sorted) {
|
|
343
|
-
this.highlightLayer.querySelectorAll('.typo-mark').forEach((mark) => {
|
|
344
|
-
const idx = Number(mark.dataset.typoIndex)
|
|
345
|
-
const edit = sorted[idx]
|
|
346
|
-
if (!edit) return
|
|
347
|
-
mark.addEventListener('mousedown', (event) => {
|
|
348
|
-
event.preventDefault()
|
|
349
|
-
this._openPopup(edit, mark)
|
|
350
|
-
})
|
|
351
|
-
})
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
_ensurePopup() {
|
|
355
|
-
if (this.popupEl) return
|
|
356
|
-
const el = document.createElement('div')
|
|
357
|
-
el.className = 'typo-popup common-popup'
|
|
358
|
-
el.style.display = 'none'
|
|
359
|
-
el.innerHTML = `
|
|
360
|
-
<input type="text" class="typo-popup-input" />
|
|
361
|
-
<ul class="typo-popup-list" data-popup-list></ul>`
|
|
362
|
-
document.body.appendChild(el)
|
|
363
|
-
this.popupEl = el
|
|
364
|
-
this.popupInput = el.querySelector('.typo-popup-input')
|
|
365
|
-
// Localized assistive label (set via setAttribute, not innerHTML, so the
|
|
366
|
-
// value is never parsed as markup).
|
|
367
|
-
this.popupInput.setAttribute('aria-label', this.labels.inputLabel)
|
|
368
|
-
|
|
369
|
-
this.popup = new CommonPopup(el, {
|
|
370
|
-
listElement: el.querySelector('.typo-popup-list'),
|
|
371
|
-
// item.label is user-typed / model-suggested text; escapeHtml round-trips
|
|
372
|
-
// it through textContent so it can never be reinterpreted as markup.
|
|
373
|
-
renderItem: (item) => {
|
|
374
|
-
const tag = item.role === 'original' ? ` <span class="typo-popup-role">(${escapeHtml(this.labels.keep)})</span>`
|
|
375
|
-
: item.role === 'custom' ? ` <span class="typo-popup-role">(${escapeHtml(this.labels.custom)})</span>` : ''
|
|
376
|
-
return `<span class="typo-popup-value">${escapeHtml(item.label)}</span>${tag}`
|
|
377
|
-
},
|
|
378
|
-
onSelect: (item) => this._chooseValue(item.value),
|
|
379
|
-
onClose: () => { this._activeEdit = null },
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
// Typing builds a creatable option and keeps input ↔ list bound.
|
|
383
|
-
this.popupInput.addEventListener('input', () => this._refreshPopupItems(this.popupInput.value))
|
|
384
|
-
this.popupInput.addEventListener('keydown', (event) => {
|
|
385
|
-
if (event.key === 'Enter') {
|
|
386
|
-
event.preventDefault()
|
|
387
|
-
this._chooseValue(this.popupInput.value)
|
|
388
|
-
return
|
|
389
|
-
}
|
|
390
|
-
if (this.popup.handleKey(event)) {
|
|
391
|
-
// Sync the input to the active list item (arrow navigation).
|
|
392
|
-
const active = this.popup.items[this.popup.activeIndex]
|
|
393
|
-
if (active) this.popupInput.value = active.value
|
|
394
|
-
}
|
|
395
|
-
})
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
_refreshPopupItems(typed) {
|
|
399
|
-
const edit = this._activeEdit
|
|
400
|
-
if (!edit) return
|
|
401
|
-
let items = buildCandidateList({
|
|
402
|
-
currentValue: edit.currentValue,
|
|
403
|
-
originalWord: edit.original,
|
|
404
|
-
suggestions: [{ suggestion: edit.suggestion, confidence: edit.confidence }],
|
|
405
|
-
})
|
|
406
|
-
const trimmed = (typed || '').trim()
|
|
407
|
-
if (trimmed && !items.some((i) => i.value === trimmed)) {
|
|
408
|
-
// Creatable: the typed word is always present and auto-selected.
|
|
409
|
-
items = [{ value: trimmed, label: trimmed, role: 'custom', isCurrent: false }, ...items]
|
|
410
|
-
}
|
|
411
|
-
this.popup.setItems(items)
|
|
412
|
-
// Auto-select the typed custom entry, else the current document value.
|
|
413
|
-
const selectIndex = trimmed
|
|
414
|
-
? Math.max(0, items.findIndex((i) => i.value === trimmed))
|
|
415
|
-
: Math.max(0, items.findIndex((i) => i.isCurrent))
|
|
416
|
-
this.popup.setActiveIndex(selectIndex)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
_openPopup(edit, anchorEl) {
|
|
420
|
-
this._ensurePopup()
|
|
421
|
-
this._activeEdit = edit
|
|
422
|
-
this.popupInput.value = edit.currentValue
|
|
423
|
-
this._refreshPopupItems('')
|
|
424
|
-
this.popup.showAt(anchorEl.getBoundingClientRect())
|
|
425
|
-
// Desktop: focus + select-all so a keystroke immediately replaces the word.
|
|
426
|
-
// Mobile keyboard is left to an explicit tap on the input (no auto-focus) —
|
|
427
|
-
// chip taps are the primary path.
|
|
428
|
-
// showAt() keeps the popup visibility:hidden until its own rAF (so it can
|
|
429
|
-
// position off-screen first); focus()/select() are no-ops while hidden, so
|
|
430
|
-
// defer them into a rAF that runs after showAt's (FIFO, registered later).
|
|
431
|
-
if (!this._isCoarsePointer()) {
|
|
432
|
-
requestAnimationFrame(() => {
|
|
433
|
-
this.popupInput.focus()
|
|
434
|
-
this.popupInput.select()
|
|
435
|
-
})
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
_isCoarsePointer() {
|
|
440
|
-
return typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
_chooseValue(value) {
|
|
444
|
-
const edit = this._activeEdit
|
|
445
|
-
if (edit == null || value == null) { this.popup?.hide(); return }
|
|
446
|
-
const replacement = String(value)
|
|
447
|
-
|
|
448
|
-
if (replacement !== edit.currentValue) {
|
|
449
|
-
const { text, delta } = applyEditAt(this.textarea.value, {
|
|
450
|
-
start: edit.start, end: edit.end, replacement,
|
|
451
|
-
})
|
|
452
|
-
let caret = this.textarea.selectionStart
|
|
453
|
-
if (edit.start < caret) caret += delta
|
|
454
|
-
this.textarea.value = text
|
|
455
|
-
this.textarea.setSelectionRange(caret, caret)
|
|
456
|
-
// Shift later edits, then drop this one (it's now user-confirmed).
|
|
457
|
-
this.edits = shiftEditsAfter(
|
|
458
|
-
this.edits.filter((e) => e !== edit),
|
|
459
|
-
edit.start,
|
|
460
|
-
delta,
|
|
461
|
-
)
|
|
462
|
-
// Our own mutation: don't re-detect (it would overwrite the remaining
|
|
463
|
-
// candidate highlights we just shifted to keep them aligned).
|
|
464
|
-
this.suppressNextDetect = true
|
|
465
|
-
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
466
|
-
} else {
|
|
467
|
-
// "Keep" — user resolved it; remove the highlight.
|
|
468
|
-
this.edits = this.edits.filter((e) => e !== edit)
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
this.popup?.hide()
|
|
472
|
-
this._repaint()
|
|
473
|
-
this.textarea.focus()
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Module-scope init, mirroring mention_menu.js. Reads settings exposed by the
|
|
478
|
-
// server on #comments-popup and only attaches when the master switch is on.
|
|
479
|
-
function readSettings(root) {
|
|
480
|
-
if (!root) return null
|
|
481
|
-
const d = root.dataset
|
|
482
|
-
if (d.typoEnabled == null) return null
|
|
483
|
-
return {
|
|
484
|
-
enabled: d.typoEnabled === 'true',
|
|
485
|
-
threshold: parseInt(d.typoThreshold, 10) || 80,
|
|
486
|
-
onVoice: d.typoOnVoice === 'true',
|
|
487
|
-
onSoftKeyboard: d.typoOnSoftKeyboard === 'true',
|
|
488
|
-
onPhysicalKeyboard: d.typoOnPhysicalKeyboard === 'true',
|
|
489
|
-
inChat: d.typoInChat === 'true',
|
|
490
|
-
inEditor: d.typoInEditor === 'true',
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
let activeInstance = null
|
|
495
|
-
|
|
496
|
-
export function initTypoCorrector() {
|
|
497
|
-
const textarea = document.querySelector('#new-comment-form textarea')
|
|
498
|
-
const root = document.getElementById('comments-popup')
|
|
499
|
-
const settings = readSettings(root)
|
|
500
|
-
if (!textarea || !settings || !settings.enabled) return null
|
|
501
|
-
if (textarea.dataset.typoBound === 'true') return null
|
|
502
|
-
textarea.dataset.typoBound = 'true'
|
|
503
|
-
|
|
504
|
-
const voiceButton = document.getElementById('voice-comments-btn')
|
|
505
|
-
activeInstance = new TypoCorrector(textarea, {
|
|
506
|
-
settings,
|
|
507
|
-
location: 'chat',
|
|
508
|
-
// Use the mounted engine path (e.g. /collavre/typo_corrections) so requests
|
|
509
|
-
// don't 404 when Collavre is mounted at a subpath instead of root.
|
|
510
|
-
endpoint: root.dataset.typoEndpoint || '/typo_corrections',
|
|
511
|
-
getVoiceActive: () => voiceButton?.dataset.voiceState === 'listening',
|
|
512
|
-
labels: {
|
|
513
|
-
keep: root.dataset.typoKeepLabel,
|
|
514
|
-
custom: root.dataset.typoCustomLabel,
|
|
515
|
-
inputLabel: root.dataset.typoInputLabel,
|
|
516
|
-
},
|
|
517
|
-
})
|
|
518
|
-
return activeInstance
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
export function teardownTypoCorrector() {
|
|
522
|
-
if (!activeInstance) return
|
|
523
|
-
activeInstance.destroy()
|
|
524
|
-
activeInstance = null
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (!initialized) {
|
|
528
|
-
initialized = true
|
|
529
|
-
document.addEventListener('turbo:load', initTypoCorrector)
|
|
530
|
-
// Turbo caches a DOM snapshot on navigation but not the JS listeners. Tear the
|
|
531
|
-
// instance down (clearing the bind marker + injected DOM) before the snapshot
|
|
532
|
-
// is taken so a restored page re-initializes cleanly instead of skipping init.
|
|
533
|
-
document.addEventListener('turbo:before-cache', teardownTypoCorrector)
|
|
534
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Collavre
|
|
2
|
-
class InboxSummaryJob < ApplicationJob
|
|
3
|
-
queue_as :default
|
|
4
|
-
|
|
5
|
-
def perform
|
|
6
|
-
Rails.logger.info("Running InboxSummaryJob")
|
|
7
|
-
User.find_each do |user|
|
|
8
|
-
if user.notifications_enabled == false
|
|
9
|
-
Rails.logger.info("Skipping inbox summary for #{user.email} because notifications are disabled")
|
|
10
|
-
next
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
items = InboxItem.where(owner: user, state: "new").order(created_at: :desc).limit(10)
|
|
14
|
-
if items.empty?
|
|
15
|
-
Rails.logger.info("No new inbox items for #{user.email}")
|
|
16
|
-
next
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
Rails.logger.info("Sending inbox summary to #{user.email} with #{items.count} items")
|
|
20
|
-
Collavre::InboxMailer.with(user: user, items: items).daily_summary.deliver_now
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
module Collavre
|
|
2
|
-
class InboxMailer < ApplicationMailer
|
|
3
|
-
def daily_summary
|
|
4
|
-
@user = params[:user]
|
|
5
|
-
@items = params[:items]
|
|
6
|
-
locale = @user.locale.presence || I18n.default_locale.to_s
|
|
7
|
-
email = I18n.with_locale(locale) do
|
|
8
|
-
mail to: @user.email, subject: I18n.t("collavre.inbox_mailer.daily_summary.subject")
|
|
9
|
-
end
|
|
10
|
-
Collavre::Email.create!(
|
|
11
|
-
user: @user,
|
|
12
|
-
email: @user.email,
|
|
13
|
-
subject: email.subject,
|
|
14
|
-
body: extract_body(email),
|
|
15
|
-
event: :inbox_summary
|
|
16
|
-
)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
module Collavre
|
|
2
|
-
class InboxItem < ApplicationRecord
|
|
3
|
-
self.table_name = "inbox_items"
|
|
4
|
-
|
|
5
|
-
# Use non-namespaced partial path for backward compatibility
|
|
6
|
-
def to_partial_path
|
|
7
|
-
"inbox_items/inbox_item"
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
INTERPOLATION_PATTERN = /%%|%\{([\w|]+)\}|%<(\w+)>[^\d]*?\d*\.?\d*[bBdiouxXeEfgGcps]/.freeze
|
|
11
|
-
|
|
12
|
-
belongs_to :owner, class_name: Collavre.configuration.user_class_name
|
|
13
|
-
belongs_to :comment, class_name: "Collavre::Comment", optional: true
|
|
14
|
-
belongs_to :creative, class_name: "Collavre::Creative", optional: true
|
|
15
|
-
|
|
16
|
-
after_commit :broadcast_badge_update, on: %i[create update destroy]
|
|
17
|
-
after_create_commit :enqueue_push_notification
|
|
18
|
-
|
|
19
|
-
enum :state, { new: "new", read: "read", archived: "archived" }, default: :new, scopes: false
|
|
20
|
-
validates :message_key, presence: true
|
|
21
|
-
|
|
22
|
-
# Kept explicit (not enum's generated `.new` class-scope, which collides with the constructor).
|
|
23
|
-
scope :new_items, -> { where(state: "new") }
|
|
24
|
-
|
|
25
|
-
def localized_message(locale: I18n.locale)
|
|
26
|
-
msg =
|
|
27
|
-
if message_key.present?
|
|
28
|
-
params = message_params || {}
|
|
29
|
-
translate_message(message_key, params.symbolize_keys, locale: locale)
|
|
30
|
-
else
|
|
31
|
-
message
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
msg&.gsub(" ", " ")&.gsub("\u00A0", " ")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def translate_message(message_key, params, locale:)
|
|
40
|
-
I18n.t(message_key, **params, locale: locale)
|
|
41
|
-
rescue I18n::MissingInterpolationArgument => e
|
|
42
|
-
missing_keys = extract_missing_keys(e.string, params)
|
|
43
|
-
fallback_params =
|
|
44
|
-
missing_keys.index_with do |missing_key|
|
|
45
|
-
default_interpolation_value(missing_key, locale: locale)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
I18n.t(message_key, **params.merge(fallback_params), locale: locale)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def extract_missing_keys(translation_string, params)
|
|
52
|
-
interpolations =
|
|
53
|
-
translation_string.to_s.scan(INTERPOLATION_PATTERN).map do |match|
|
|
54
|
-
match.compact.first&.to_sym
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
interpolations.compact.uniq - params.keys
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def default_interpolation_value(key, locale:)
|
|
61
|
-
case key.to_sym
|
|
62
|
-
when :comment_content
|
|
63
|
-
I18n.t("collavre.inbox.comment_content_unavailable", locale: locale, default: "")
|
|
64
|
-
else
|
|
65
|
-
""
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def broadcast_badge_update
|
|
70
|
-
# Recompute the new count for this owner:
|
|
71
|
-
new_count = InboxItem.where(owner: owner, state: "new").count
|
|
72
|
-
|
|
73
|
-
# Use Turbo::StreamsChannel to broadcast replace to that user's inbox stream:
|
|
74
|
-
%w[desktop-inbox-badge mobile-inbox-badge].each do |target_id|
|
|
75
|
-
Turbo::StreamsChannel.broadcast_replace_to(
|
|
76
|
-
[ "inbox", owner ],
|
|
77
|
-
target: target_id,
|
|
78
|
-
partial: "inbox/badge_component/count",
|
|
79
|
-
locals: { count: new_count, badge_id: target_id, show_zero: false }
|
|
80
|
-
)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def enqueue_push_notification
|
|
85
|
-
msg = localized_message(locale: owner.locale || "en")
|
|
86
|
-
PushNotificationJob.perform_later(owner_id, message: msg, link: link)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|