collavre 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +221 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/stylesheets/collavre/actiontext.css +577 -0
  5. data/app/assets/stylesheets/collavre/activity_logs.css +99 -0
  6. data/app/assets/stylesheets/collavre/comments_popup.css +692 -0
  7. data/app/assets/stylesheets/collavre/creatives.css +559 -0
  8. data/app/assets/stylesheets/collavre/dark_mode.css +118 -0
  9. data/app/assets/stylesheets/collavre/mention_menu.css +43 -0
  10. data/app/assets/stylesheets/collavre/popup.css +160 -0
  11. data/app/assets/stylesheets/collavre/print.css +37 -0
  12. data/app/assets/stylesheets/collavre/slide_view.css +79 -0
  13. data/app/assets/stylesheets/collavre/user_menu.css +34 -0
  14. data/app/channels/collavre/comments_presence_channel.rb +54 -0
  15. data/app/channels/collavre/slide_view_channel.rb +11 -0
  16. data/app/channels/collavre/topics_channel.rb +12 -0
  17. data/app/components/collavre/avatar_component.html.erb +15 -0
  18. data/app/components/collavre/avatar_component.rb +59 -0
  19. data/app/components/collavre/inbox/badge_component.html.erb +6 -0
  20. data/app/components/collavre/inbox/badge_component.rb +18 -0
  21. data/app/components/collavre/plans_timeline_component.html.erb +14 -0
  22. data/app/components/collavre/plans_timeline_component.rb +56 -0
  23. data/app/components/collavre/popup_menu_component.html.erb +6 -0
  24. data/app/components/collavre/popup_menu_component.rb +30 -0
  25. data/app/components/collavre/progress_filter_component.html.erb +5 -0
  26. data/app/components/collavre/progress_filter_component.rb +10 -0
  27. data/app/components/collavre/user_mention_menu_component.html.erb +3 -0
  28. data/app/components/collavre/user_mention_menu_component.rb +8 -0
  29. data/app/controllers/collavre/application_controller.rb +15 -0
  30. data/app/controllers/collavre/attachments_controller.rb +44 -0
  31. data/app/controllers/collavre/calendar_events_controller.rb +15 -0
  32. data/app/controllers/collavre/comment_read_pointers_controller.rb +80 -0
  33. data/app/controllers/collavre/comments/activity_logs_controller.rb +26 -0
  34. data/app/controllers/collavre/comments/reactions_controller.rb +82 -0
  35. data/app/controllers/collavre/comments_controller.rb +464 -0
  36. data/app/controllers/collavre/contacts_controller.rb +10 -0
  37. data/app/controllers/collavre/creative_expanded_states_controller.rb +27 -0
  38. data/app/controllers/collavre/creative_imports_controller.rb +24 -0
  39. data/app/controllers/collavre/creative_plans_controller.rb +69 -0
  40. data/app/controllers/collavre/creative_shares_controller.rb +79 -0
  41. data/app/controllers/collavre/creatives_controller.rb +535 -0
  42. data/app/controllers/collavre/devices_controller.rb +19 -0
  43. data/app/controllers/collavre/email_verifications_controller.rb +16 -0
  44. data/app/controllers/collavre/emails_controller.rb +11 -0
  45. data/app/controllers/collavre/github_auth_controller.rb +25 -0
  46. data/app/controllers/collavre/google_auth_controller.rb +43 -0
  47. data/app/controllers/collavre/inbox_items_controller.rb +64 -0
  48. data/app/controllers/collavre/invites_controller.rb +27 -0
  49. data/app/controllers/collavre/notion_auth_controller.rb +25 -0
  50. data/app/controllers/collavre/passwords_controller.rb +37 -0
  51. data/app/controllers/collavre/plans_controller.rb +110 -0
  52. data/app/controllers/collavre/sessions_controller.rb +57 -0
  53. data/app/controllers/collavre/topics_controller.rb +58 -0
  54. data/app/controllers/collavre/user_themes_controller.rb +58 -0
  55. data/app/controllers/collavre/users_controller.rb +390 -0
  56. data/app/helpers/collavre/application_helper.rb +4 -0
  57. data/app/helpers/collavre/comments_helper.rb +9 -0
  58. data/app/helpers/collavre/creatives_helper.rb +343 -0
  59. data/app/helpers/collavre/navigation_helper.rb +163 -0
  60. data/app/helpers/collavre/user_themes_helper.rb +4 -0
  61. data/app/javascript/collavre.js +26 -0
  62. data/app/javascript/components/InlineLexicalEditor.jsx +889 -0
  63. data/app/javascript/components/LinkPopup.jsx +112 -0
  64. data/app/javascript/components/creative_tree_row.js +503 -0
  65. data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +95 -0
  66. data/app/javascript/components/plugins/image_upload_plugin.jsx +162 -0
  67. data/app/javascript/controllers/click_target_controller.js +13 -0
  68. data/app/javascript/controllers/comment_controller.js +162 -0
  69. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +68 -0
  70. data/app/javascript/controllers/comments/form_controller.js +530 -0
  71. data/app/javascript/controllers/comments/list_controller.js +715 -0
  72. data/app/javascript/controllers/comments/mention_menu_controller.js +41 -0
  73. data/app/javascript/controllers/comments/popup_controller.js +385 -0
  74. data/app/javascript/controllers/comments/presence_controller.js +311 -0
  75. data/app/javascript/controllers/comments/topics_controller.js +338 -0
  76. data/app/javascript/controllers/common_popup_controller.js +55 -0
  77. data/app/javascript/controllers/creatives/drag_drop_controller.js +45 -0
  78. data/app/javascript/controllers/creatives/expansion_controller.js +222 -0
  79. data/app/javascript/controllers/creatives/import_controller.js +116 -0
  80. data/app/javascript/controllers/creatives/row_editor_controller.js +8 -0
  81. data/app/javascript/controllers/creatives/select_mode_controller.js +231 -0
  82. data/app/javascript/controllers/creatives/set_plan_modal_controller.js +107 -0
  83. data/app/javascript/controllers/creatives/tree_controller.js +218 -0
  84. data/app/javascript/controllers/index.js +79 -0
  85. data/app/javascript/controllers/link_creative_controller.js +91 -0
  86. data/app/javascript/controllers/popup_menu_controller.js +82 -0
  87. data/app/javascript/controllers/progress_filter_controller.js +35 -0
  88. data/app/javascript/controllers/reaction_picker_controller.js +107 -0
  89. data/app/javascript/controllers/share_invite_controller.js +15 -0
  90. data/app/javascript/controllers/share_user_search_controller.js +121 -0
  91. data/app/javascript/controllers/tabs_controller.js +43 -0
  92. data/app/javascript/creatives/drag_drop/dom.js +170 -0
  93. data/app/javascript/creatives/drag_drop/event_handlers.js +846 -0
  94. data/app/javascript/creatives/drag_drop/indicator.js +35 -0
  95. data/app/javascript/creatives/drag_drop/operations.js +116 -0
  96. data/app/javascript/creatives/drag_drop/state.js +31 -0
  97. data/app/javascript/creatives/tree_renderer.js +248 -0
  98. data/app/javascript/lib/api/__tests__/queue_manager.test.js +153 -0
  99. data/app/javascript/lib/api/creatives.js +79 -0
  100. data/app/javascript/lib/api/csrf_fetch.js +22 -0
  101. data/app/javascript/lib/api/drag_drop.js +31 -0
  102. data/app/javascript/lib/api/queue_manager.js +423 -0
  103. data/app/javascript/lib/apply_lexical_styles.js +15 -0
  104. data/app/javascript/lib/common_popup.js +195 -0
  105. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +91 -0
  106. data/app/javascript/lib/lexical/__tests__/attachment_payload.test.js +194 -0
  107. data/app/javascript/lib/lexical/action_text_attachment_node.js +459 -0
  108. data/app/javascript/lib/lexical/attachment_node.jsx +170 -0
  109. data/app/javascript/lib/lexical/attachment_payload.js +293 -0
  110. data/app/javascript/lib/lexical/dom_attachment_utils.js +66 -0
  111. data/app/javascript/lib/lexical/image_node.jsx +159 -0
  112. data/app/javascript/lib/lexical/style_attributes.js +40 -0
  113. data/app/javascript/lib/responsive_images.js +54 -0
  114. data/app/javascript/lib/turbo_stream_actions.js +33 -0
  115. data/app/javascript/lib/utils/markdown.js +23 -0
  116. data/app/javascript/modules/creative_guide.js +53 -0
  117. data/app/javascript/modules/creative_row_editor.js +1841 -0
  118. data/app/javascript/modules/creative_row_swipe.js +43 -0
  119. data/app/javascript/modules/creatives.js +15 -0
  120. data/app/javascript/modules/export_to_markdown.js +34 -0
  121. data/app/javascript/modules/inbox_panel.js +226 -0
  122. data/app/javascript/modules/lexical_inline_editor.jsx +133 -0
  123. data/app/javascript/modules/mention_menu.js +77 -0
  124. data/app/javascript/modules/plans_menu.js +39 -0
  125. data/app/javascript/modules/plans_timeline.js +397 -0
  126. data/app/javascript/modules/share_modal.js +73 -0
  127. data/app/javascript/modules/share_user_popup.js +77 -0
  128. data/app/javascript/modules/slide_view.js +163 -0
  129. data/app/javascript/services/cable.js +32 -0
  130. data/app/javascript/slide_view.js +2 -0
  131. data/app/javascript/utils/caret_position.js +42 -0
  132. data/app/javascript/utils/clipboard.js +40 -0
  133. data/app/jobs/collavre/ai_agent_job.rb +27 -0
  134. data/app/jobs/collavre/inbox_summary_job.rb +24 -0
  135. data/app/jobs/collavre/notion_export_job.rb +30 -0
  136. data/app/jobs/collavre/notion_sync_job.rb +48 -0
  137. data/app/jobs/collavre/permission_cache_cleanup_job.rb +36 -0
  138. data/app/jobs/collavre/permission_cache_job.rb +71 -0
  139. data/app/jobs/collavre/push_notification_job.rb +86 -0
  140. data/app/mailers/collavre/application_mailer.rb +17 -0
  141. data/app/mailers/collavre/creative_mailer.rb +9 -0
  142. data/app/mailers/collavre/email_verification_mailer.rb +20 -0
  143. data/app/mailers/collavre/inbox_mailer.rb +19 -0
  144. data/app/mailers/collavre/invitation_mailer.rb +16 -0
  145. data/app/mailers/collavre/passwords_mailer.rb +10 -0
  146. data/app/models/collavre/activity_log.rb +13 -0
  147. data/app/models/collavre/application_record.rb +5 -0
  148. data/app/models/collavre/calendar_event.rb +20 -0
  149. data/app/models/collavre/comment.rb +307 -0
  150. data/app/models/collavre/comment_presence_store.rb +30 -0
  151. data/app/models/collavre/comment_reaction.rb +11 -0
  152. data/app/models/collavre/comment_read_pointer.rb +26 -0
  153. data/app/models/collavre/contact.rb +23 -0
  154. data/app/models/collavre/creative.rb +413 -0
  155. data/app/models/collavre/creative_expanded_state.rb +11 -0
  156. data/app/models/collavre/creative_share.rb +122 -0
  157. data/app/models/collavre/creative_shares_cache.rb +18 -0
  158. data/app/models/collavre/current.rb +14 -0
  159. data/app/models/collavre/device.rb +13 -0
  160. data/app/models/collavre/email.rb +14 -0
  161. data/app/models/collavre/github_account.rb +10 -0
  162. data/app/models/collavre/github_repository_link.rb +19 -0
  163. data/app/models/collavre/inbox_item.rb +95 -0
  164. data/app/models/collavre/invitation.rb +22 -0
  165. data/app/models/collavre/label.rb +47 -0
  166. data/app/models/collavre/mcp_tool.rb +30 -0
  167. data/app/models/collavre/notion_account.rb +17 -0
  168. data/app/models/collavre/notion_block_link.rb +10 -0
  169. data/app/models/collavre/notion_page_link.rb +19 -0
  170. data/app/models/collavre/plan.rb +20 -0
  171. data/app/models/collavre/session.rb +24 -0
  172. data/app/models/collavre/system_setting.rb +144 -0
  173. data/app/models/collavre/tag.rb +10 -0
  174. data/app/models/collavre/task.rb +10 -0
  175. data/app/models/collavre/task_action.rb +10 -0
  176. data/app/models/collavre/topic.rb +12 -0
  177. data/app/models/collavre/user.rb +174 -0
  178. data/app/models/collavre/user_theme.rb +10 -0
  179. data/app/models/collavre/variation.rb +5 -0
  180. data/app/models/collavre/webauthn_credential.rb +11 -0
  181. data/app/services/collavre/ai_agent_service.rb +193 -0
  182. data/app/services/collavre/ai_client.rb +183 -0
  183. data/app/services/collavre/ai_system_prompt_renderer.rb +38 -0
  184. data/app/services/collavre/auto_theme_generator.rb +198 -0
  185. data/app/services/collavre/comment_link_formatter.rb +60 -0
  186. data/app/services/collavre/comments/action_executor.rb +262 -0
  187. data/app/services/collavre/comments/action_validator.rb +58 -0
  188. data/app/services/collavre/comments/calendar_command.rb +97 -0
  189. data/app/services/collavre/comments/command_processor.rb +37 -0
  190. data/app/services/collavre/comments/mcp_command.rb +109 -0
  191. data/app/services/collavre/comments/mcp_command_builder.rb +32 -0
  192. data/app/services/collavre/creatives/filter_pipeline.rb +196 -0
  193. data/app/services/collavre/creatives/filters/assignee_filter.rb +30 -0
  194. data/app/services/collavre/creatives/filters/base_filter.rb +24 -0
  195. data/app/services/collavre/creatives/filters/comment_filter.rb +21 -0
  196. data/app/services/collavre/creatives/filters/date_filter.rb +58 -0
  197. data/app/services/collavre/creatives/filters/progress_filter.rb +25 -0
  198. data/app/services/collavre/creatives/filters/search_filter.rb +28 -0
  199. data/app/services/collavre/creatives/filters/tag_filter.rb +16 -0
  200. data/app/services/collavre/creatives/importer.rb +47 -0
  201. data/app/services/collavre/creatives/index_query.rb +191 -0
  202. data/app/services/collavre/creatives/path_exporter.rb +131 -0
  203. data/app/services/collavre/creatives/permission_cache_builder.rb +194 -0
  204. data/app/services/collavre/creatives/permission_checker.rb +42 -0
  205. data/app/services/collavre/creatives/plan_tagger.rb +53 -0
  206. data/app/services/collavre/creatives/progress_service.rb +89 -0
  207. data/app/services/collavre/creatives/reorderer.rb +187 -0
  208. data/app/services/collavre/creatives/tree_builder.rb +231 -0
  209. data/app/services/collavre/creatives/tree_formatter.rb +36 -0
  210. data/app/services/collavre/gemini_parent_recommender.rb +77 -0
  211. data/app/services/collavre/github/client.rb +112 -0
  212. data/app/services/collavre/github/pull_request_analyzer.rb +280 -0
  213. data/app/services/collavre/github/pull_request_processor.rb +181 -0
  214. data/app/services/collavre/github/webhook_provisioner.rb +130 -0
  215. data/app/services/collavre/google_calendar_service.rb +149 -0
  216. data/app/services/collavre/link_preview_fetcher.rb +230 -0
  217. data/app/services/collavre/markdown_importer.rb +202 -0
  218. data/app/services/collavre/mcp_service.rb +217 -0
  219. data/app/services/collavre/notion_client.rb +231 -0
  220. data/app/services/collavre/notion_creative_exporter.rb +296 -0
  221. data/app/services/collavre/notion_service.rb +249 -0
  222. data/app/services/collavre/ppt_importer.rb +76 -0
  223. data/app/services/collavre/ruby_llm_interaction_logger.rb +34 -0
  224. data/app/services/collavre/system_events/context_builder.rb +41 -0
  225. data/app/services/collavre/system_events/dispatcher.rb +19 -0
  226. data/app/services/collavre/system_events/router.rb +72 -0
  227. data/app/services/collavre/tools/creative_retrieval_service.rb +138 -0
  228. data/app/views/admin/shared/_tabs.html.erb +4 -0
  229. data/app/views/collavre/comments/_activity_log_details.html.erb +23 -0
  230. data/app/views/collavre/comments/_comment.html.erb +147 -0
  231. data/app/views/collavre/comments/_comments_popup.html.erb +46 -0
  232. data/app/views/collavre/comments/_list.html.erb +10 -0
  233. data/app/views/collavre/comments/_presence_avatars.html.erb +8 -0
  234. data/app/views/collavre/comments/_reaction_picker.html.erb +15 -0
  235. data/app/views/collavre/comments/_read_receipts.html.erb +19 -0
  236. data/app/views/collavre/creatives/_add_button.html.erb +20 -0
  237. data/app/views/collavre/creatives/_delete_button.html.erb +12 -0
  238. data/app/views/collavre/creatives/_github_integration_modal.html.erb +77 -0
  239. data/app/views/collavre/creatives/_import_upload_zone.html.erb +10 -0
  240. data/app/views/collavre/creatives/_inline_edit_form.html.erb +89 -0
  241. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +24 -0
  242. data/app/views/collavre/creatives/_notion_integration_modal.html.erb +90 -0
  243. data/app/views/collavre/creatives/_set_plan_modal.html.erb +25 -0
  244. data/app/views/collavre/creatives/_share_button.html.erb +55 -0
  245. data/app/views/collavre/creatives/edit.html.erb +4 -0
  246. data/app/views/collavre/creatives/index.html.erb +192 -0
  247. data/app/views/collavre/creatives/new.html.erb +4 -0
  248. data/app/views/collavre/creatives/show.html.erb +29 -0
  249. data/app/views/collavre/creatives/slide_view.html.erb +20 -0
  250. data/app/views/collavre/email_verification_mailer/verify.html.erb +4 -0
  251. data/app/views/collavre/email_verification_mailer/verify.text.erb +2 -0
  252. data/app/views/collavre/emails/index.html.erb +19 -0
  253. data/app/views/collavre/emails/show.html.erb +5 -0
  254. data/app/views/collavre/inbox_items/_item.html.erb +18 -0
  255. data/app/views/collavre/inbox_items/_items.html.erb +3 -0
  256. data/app/views/collavre/inbox_items/_list.html.erb +7 -0
  257. data/app/views/collavre/inbox_items/index.html.erb +1 -0
  258. data/app/views/collavre/inbox_mailer/daily_summary.html.erb +11 -0
  259. data/app/views/collavre/inbox_mailer/daily_summary.text.erb +8 -0
  260. data/app/views/collavre/invitation_mailer/invite.html.erb +5 -0
  261. data/app/views/collavre/invitation_mailer/invite.text.erb +3 -0
  262. data/app/views/collavre/invites/show.html.erb +8 -0
  263. data/app/views/collavre/passwords/edit.html.erb +12 -0
  264. data/app/views/collavre/passwords/new.html.erb +17 -0
  265. data/app/views/collavre/passwords_mailer/reset.html.erb +4 -0
  266. data/app/views/collavre/passwords_mailer/reset.text.erb +2 -0
  267. data/app/views/collavre/sessions/new.html.erb +27 -0
  268. data/app/views/collavre/sessions/providers/_google.html.erb +7 -0
  269. data/app/views/collavre/sessions/providers/_passkey.html.erb +3 -0
  270. data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -0
  271. data/app/views/collavre/shared/_navigation.html.erb +37 -0
  272. data/app/views/collavre/shared/navigation/_help_button.html.erb +1 -0
  273. data/app/views/collavre/shared/navigation/_inbox_button.html.erb +5 -0
  274. data/app/views/collavre/shared/navigation/_mobile_inbox_button.html.erb +3 -0
  275. data/app/views/collavre/shared/navigation/_mobile_plans_button.html.erb +1 -0
  276. data/app/views/collavre/shared/navigation/_panels.html.erb +18 -0
  277. data/app/views/collavre/shared/navigation/_plans_button.html.erb +3 -0
  278. data/app/views/collavre/shared/navigation/_search_form.html.erb +6 -0
  279. data/app/views/collavre/user_themes/index.html.erb +75 -0
  280. data/app/views/collavre/users/_app_header.html.erb +5 -0
  281. data/app/views/collavre/users/_contact_management.html.erb +77 -0
  282. data/app/views/collavre/users/_id_pwd_fields.html.erb +27 -0
  283. data/app/views/collavre/users/edit_ai.html.erb +79 -0
  284. data/app/views/collavre/users/edit_password.html.erb +27 -0
  285. data/app/views/collavre/users/index.html.erb +80 -0
  286. data/app/views/collavre/users/new.html.erb +33 -0
  287. data/app/views/collavre/users/new_ai.html.erb +87 -0
  288. data/app/views/collavre/users/passkeys.html.erb +43 -0
  289. data/app/views/collavre/users/show.html.erb +143 -0
  290. data/app/views/inbox/badge_component/_count.html.erb +2 -0
  291. data/app/views/layouts/collavre/slide.html.erb +29 -0
  292. data/config/locales/comments.en.yml +114 -0
  293. data/config/locales/comments.ko.yml +110 -0
  294. data/config/locales/contacts.en.yml +16 -0
  295. data/config/locales/contacts.ko.yml +16 -0
  296. data/config/locales/creatives.en.yml +183 -0
  297. data/config/locales/creatives.ko.yml +164 -0
  298. data/config/locales/invites.en.yml +19 -0
  299. data/config/locales/invites.ko.yml +17 -0
  300. data/config/locales/notifications.en.yml +8 -0
  301. data/config/locales/notifications.ko.yml +8 -0
  302. data/config/locales/plans.en.yml +12 -0
  303. data/config/locales/plans.ko.yml +19 -0
  304. data/config/locales/themes.en.yml +29 -0
  305. data/config/locales/themes.ko.yml +27 -0
  306. data/config/locales/users.en.yml +151 -0
  307. data/config/locales/users.ko.yml +146 -0
  308. data/config/routes.rb +92 -0
  309. data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
  310. data/db/migrate/20250128110017_create_creatives.rb +11 -0
  311. data/db/migrate/20250128120122_create_users.rb +11 -0
  312. data/db/migrate/20250128120123_create_sessions.rb +11 -0
  313. data/db/migrate/20250128123633_create_subscribers.rb +10 -0
  314. data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
  315. data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
  316. data/db/migrate/20250522115048_remove_name_from_creatives.rb +5 -0
  317. data/db/migrate/20250522190651_add_parent_id_to_creatives.rb +5 -0
  318. data/db/migrate/20250523133100_rename_inventory_count_to_progress.rb +13 -0
  319. data/db/migrate/20250523133101_add_sequence_to_creatives.rb +5 -0
  320. data/db/migrate/20250525205100_add_user_id_to_creatives.rb +10 -0
  321. data/db/migrate/20250527014217_create_creative_shares.rb +12 -0
  322. data/db/migrate/20250528142349_add_origin_id_to_creatives.rb +5 -0
  323. data/db/migrate/20250530060200_create_tags.rb +9 -0
  324. data/db/migrate/20250531105150_add_value_to_tags.rb +5 -0
  325. data/db/migrate/20250531140142_create_labels.rb +12 -0
  326. data/db/migrate/20250531140145_change_tag_to_label_reference.rb +6 -0
  327. data/db/migrate/20250601000000_create_comments.rb +10 -0
  328. data/db/migrate/20250601061830_drop_plans_and_variations.rb +6 -0
  329. data/db/migrate/20250604122600_add_owner_to_labels.rb +5 -0
  330. data/db/migrate/20250606000000_rename_email_address_to_email_in_users.rb +7 -0
  331. data/db/migrate/20250606150329_create_creative_hierarchies.rb +16 -0
  332. data/db/migrate/20250610142000_create_creative_expanded_states.rb +11 -0
  333. data/db/migrate/20250611030138_create_invitations.rb +15 -0
  334. data/db/migrate/20250611105524_create_inbox_items.rb +14 -0
  335. data/db/migrate/20250612150000_update_permissions.rb +43 -0
  336. data/db/migrate/20250612232913_add_email_verified_at_to_users.rb +5 -0
  337. data/db/migrate/20250616065905_add_avatar_to_users.rb +5 -0
  338. data/db/migrate/20250617092111_add_display_level_to_users.rb +5 -0
  339. data/db/migrate/20250620004558_create_emails.rb +13 -0
  340. data/db/migrate/20250621000000_create_comment_read_pointers.rb +11 -0
  341. data/db/migrate/20250622000000_add_completion_mark_to_users.rb +5 -0
  342. data/db/migrate/20250623000000_add_theme_to_users.rb +5 -0
  343. data/db/migrate/20250624000000_add_name_to_users.rb +18 -0
  344. data/db/migrate/20250714190000_create_devices.rb +17 -0
  345. data/db/migrate/20250715120000_add_notifications_enabled_to_users.rb +5 -0
  346. data/db/migrate/20250823000000_add_no_access_permission.rb +25 -0
  347. data/db/migrate/20250826000000_add_calendar_id_to_users.rb +5 -0
  348. data/db/migrate/20250827000000_add_google_oauth_tokens_to_users.rb +8 -0
  349. data/db/migrate/20250827061238_add_timezone_to_users.rb +5 -0
  350. data/db/migrate/20250828000000_create_calendar_events.rb +16 -0
  351. data/db/migrate/20250828060000_add_creative_to_calendar_events.rb +5 -0
  352. data/db/migrate/20250830141052_add_locale_to_users.rb +5 -0
  353. data/db/migrate/20250830141101_add_message_key_to_inbox_items.rb +6 -0
  354. data/db/migrate/20250902025423_remove_description_and_featured_image_from_creatives.rb +6 -0
  355. data/db/migrate/20250910000000_add_private_to_comments.rb +5 -0
  356. data/db/migrate/20250910105640_migrate_write_permissions_to_admin.rb +17 -0
  357. data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +27 -0
  358. data/db/migrate/20250923002959_deduplicate_device_fcm_tokens.rb +25 -0
  359. data/db/migrate/20250924000000_add_system_admin_to_users.rb +6 -0
  360. data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
  361. data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
  362. data/db/migrate/20250928000000_add_action_and_approver_to_comments.rb +6 -0
  363. data/db/migrate/20250928010000_add_action_execution_tracking_to_comments.rb +6 -0
  364. data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
  365. data/db/migrate/20250929000000_add_comment_and_creative_refs_to_inbox_items.rb +6 -0
  366. data/db/migrate/20251001000001_create_contacts.rb +71 -0
  367. data/db/migrate/20251002000000_add_shared_by_to_creative_shares.rb +14 -0
  368. data/db/migrate/20251124120902_add_ai_fields_to_users.rb +7 -0
  369. data/db/migrate/20251124122218_add_created_by_id_to_users.rb +5 -0
  370. data/db/migrate/20251124124521_add_llm_api_key_to_users.rb +5 -0
  371. data/db/migrate/20251124130000_add_searchable_to_users.rb +6 -0
  372. data/db/migrate/20251125072705_migrate_linked_creative_children_to_origin.rb +28 -0
  373. data/db/migrate/20251126040752_add_description_to_creatives.rb +75 -0
  374. data/db/migrate/20251127000000_move_comments_from_linked_creatives_to_origins.rb +18 -0
  375. data/db/migrate/20251201073823_add_tools_to_users.rb +5 -0
  376. data/db/migrate/20251202062715_create_mcp_tools.rb +16 -0
  377. data/db/migrate/20251204125754_create_tasks_and_task_actions.rb +23 -0
  378. data/db/migrate/20251204125756_add_routing_expression_to_users.rb +5 -0
  379. data/db/migrate/20251204161133_set_default_routing_expression_for_ai_agents.rb +13 -0
  380. data/db/migrate/20251211033025_nullify_self_referencing_origins.rb +9 -0
  381. data/db/migrate/20251211080040_create_topics_and_add_topic_to_comments.rb +15 -0
  382. data/db/migrate/20251215143500_create_activity_logs.rb +16 -0
  383. data/db/migrate/20251222125727_create_webauthn_credentials.rb +14 -0
  384. data/db/migrate/20251222125839_add_webauthn_id_to_users.rb +5 -0
  385. data/db/migrate/20251223022315_create_comment_reactions.rb +13 -0
  386. data/db/migrate/20251223072625_create_user_themes.rb +11 -0
  387. data/db/migrate/20251230074456_add_creative_id_to_labels.rb +5 -0
  388. data/db/migrate/20251230113607_refactor_labels.rb +38 -0
  389. data/db/migrate/20251231010012_backfill_tags_for_labels.rb +15 -0
  390. data/db/migrate/20251231013234_drop_subscribers.rb +9 -0
  391. data/db/migrate/20260106090544_allow_null_user_id_in_creative_shares.rb +6 -0
  392. data/db/migrate/20260106160643_create_system_settings.rb +11 -0
  393. data/db/migrate/20260116000000_create_creative_shares_cache.rb +15 -0
  394. data/db/migrate/20260116000001_populate_creative_shares_cache.rb +23 -0
  395. data/db/migrate/20260119022933_make_source_share_id_nullable_in_creative_shares_caches.rb +7 -0
  396. data/db/migrate/20260119023446_populate_owner_cache_entries.rb +25 -0
  397. data/db/migrate/20260119100000_add_account_lockout_to_users.rb +6 -0
  398. data/db/migrate/20260119110000_add_last_active_at_to_sessions.rb +6 -0
  399. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +60 -0
  400. data/db/migrate/20260120162259_remove_fk_from_creative_shares_caches.rb +7 -0
  401. data/db/migrate/20260120163856_remove_timestamps_from_creative_shares_caches.rb +6 -0
  402. data/lib/collavre/configuration.rb +14 -0
  403. data/lib/collavre/engine.rb +77 -0
  404. data/lib/collavre/user_extensions.rb +29 -0
  405. data/lib/collavre/version.rb +3 -0
  406. data/lib/collavre.rb +26 -0
  407. data/lib/generators/collavre/install/install_generator.rb +105 -0
  408. data/lib/generators/collavre/install/templates/build.cjs.tt +100 -0
  409. data/lib/tasks/collavre_assets.rake +15 -0
  410. metadata +591 -0
@@ -0,0 +1,231 @@
1
+ module Collavre
2
+ module Creatives
3
+ class TreeBuilder
4
+ FILTER_IGNORED_KEYS = %w[id controller action format level select_mode].freeze
5
+
6
+ def initialize(user:, params:, view_context:, expanded_state_map:, select_mode:, max_level:, allowed_creative_ids: nil, progress_map: nil)
7
+ @user = user
8
+ @view_context = view_context
9
+ @raw_params = params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params.to_h
10
+ @expanded_state_map = expanded_state_map || {}
11
+ @select_mode = select_mode
12
+ @max_level = max_level
13
+ @allowed_creative_ids = allowed_creative_ids
14
+ @progress_map = progress_map
15
+ @permission_cache = {}
16
+ end
17
+
18
+ def build(collection, level: 1)
19
+ return [] if collection.blank?
20
+
21
+ creatives = Array(collection)
22
+ # Preload permissions for all creatives in this batch to avoid N+1
23
+ preload_permissions(creatives)
24
+
25
+ build_nodes(creatives, level: level)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :user, :view_context, :raw_params, :expanded_state_map, :select_mode, :max_level, :allowed_creative_ids, :progress_map, :permission_cache
31
+
32
+ def preload_permissions(creatives)
33
+ return unless user
34
+
35
+ creative_ids = creatives.map(&:id)
36
+ return if creative_ids.empty?
37
+
38
+ # Skip if already cached
39
+ uncached_ids = creative_ids - @permission_cache.keys
40
+ return if uncached_ids.empty?
41
+
42
+ write_rank = CreativeShare.permissions[:write]
43
+
44
+ # Batch query for user-specific permissions
45
+ user_permissions = CreativeSharesCache
46
+ .where(creative_id: uncached_ids, user_id: user.id)
47
+ .pluck(:creative_id, :permission)
48
+
49
+ user_permissions.each do |cid, perm|
50
+ perm_rank = CreativeSharesCache.permissions[perm]
51
+ @permission_cache[cid] = perm_rank && perm_rank >= write_rank && perm_rank != CreativeSharesCache.permissions[:no_access]
52
+ end
53
+
54
+ # For remaining uncached, check public shares
55
+ still_uncached = uncached_ids - @permission_cache.keys
56
+ if still_uncached.any?
57
+ public_permissions = CreativeSharesCache
58
+ .where(creative_id: still_uncached, user_id: nil)
59
+ .pluck(:creative_id, :permission)
60
+
61
+ public_permissions.each do |cid, perm|
62
+ next if @permission_cache.key?(cid)
63
+ perm_rank = CreativeSharesCache.permissions[perm]
64
+ @permission_cache[cid] = perm_rank && perm_rank >= write_rank && perm_rank != CreativeSharesCache.permissions[:no_access]
65
+ end
66
+ end
67
+
68
+ # Mark owned creatives as having write permission
69
+ owned_ids = creatives.select { |c| c.user_id == user.id }.map(&:id)
70
+ owned_ids.each { |cid| @permission_cache[cid] = true }
71
+
72
+ # Default to false for any remaining uncached
73
+ uncached_ids.each { |cid| @permission_cache[cid] ||= false }
74
+ end
75
+
76
+ def cached_can_write?(creative)
77
+ return false unless user
78
+
79
+ if @permission_cache.key?(creative.id)
80
+ @permission_cache[creative.id]
81
+ else
82
+ # Fallback to individual check if not cached
83
+ creative.has_permission?(user, :write)
84
+ end
85
+ end
86
+
87
+ def build_nodes(creatives, level:)
88
+ return [] if level > max_level
89
+
90
+ # Preload permissions for this batch of creatives
91
+ preload_permissions(creatives) if creatives.any?
92
+
93
+ creatives.flat_map do |creative|
94
+ build_nodes_for_creative(creative, level: level)
95
+ end
96
+ end
97
+
98
+ def build_nodes_for_creative(creative, level:)
99
+ if progress_map && progress_map.key?(creative.id.to_s)
100
+ creative.filtered_progress = progress_map[creative.id.to_s]
101
+ end
102
+
103
+ filtered_children = filtered_children_for(creative)
104
+ expanded = expanded?(creative.id)
105
+ skip = skip_creative?(creative)
106
+ child_level = level + 1
107
+ child_render_level = skip ? level : child_level
108
+ load_children_now = filters_applied? || expanded || skip
109
+ children_nodes = load_children_now ? build_nodes(filtered_children, level: child_render_level) : []
110
+
111
+ return children_nodes if skip
112
+
113
+ [
114
+ {
115
+ id: creative.id,
116
+ dom_id: "creative-#{creative.id}",
117
+ parent_id: creative.parent_id,
118
+ level: level,
119
+ select_mode: !!select_mode,
120
+ can_write: cached_can_write?(creative),
121
+ has_children: filtered_children.any?,
122
+ expanded: expanded,
123
+ is_root: creative.parent.nil?,
124
+ link_url: view_context.collavre.creative_path(creative),
125
+ templates: template_payload_for(creative),
126
+ inline_editor_payload: inline_editor_payload_for(creative),
127
+ children_container: children_container_payload(
128
+ creative,
129
+ filtered_children,
130
+ child_level: child_level,
131
+ children_nodes: children_nodes,
132
+ expanded: expanded,
133
+ load_children_now: load_children_now
134
+ )
135
+ }
136
+ ]
137
+ end
138
+
139
+ def skip_creative?(creative)
140
+ # With FilterPipeline, allowed_creative_ids contains all creatives that should be shown
141
+ # (matched items + their ancestors). No need for duplicate filter logic here.
142
+ return !allowed_creative_ids.include?(creative.id.to_s) if allowed_creative_ids
143
+
144
+ false
145
+ end
146
+
147
+ def filtered_children_for(creative)
148
+ return [] if raw_params["comment"] == "true" || raw_params["search"].present?
149
+
150
+ children = creative.children_with_permission(user)
151
+ if allowed_creative_ids
152
+ children.select { |c| allowed_creative_ids.include?(c.id.to_s) }
153
+ else
154
+ children
155
+ end
156
+ end
157
+
158
+ def expanded?(creative_id)
159
+ expanded_state_map[creative_id.to_s].present?
160
+ end
161
+
162
+ def filters_applied?
163
+ @filters_applied ||= begin
164
+ return true if allowed_creative_ids.present?
165
+
166
+ filtered = raw_params.except(*FILTER_IGNORED_KEYS)
167
+ filtered.present?
168
+ end
169
+ end
170
+
171
+ def template_payload_for(creative)
172
+ description_html = view_context.embed_youtube_iframe(creative.effective_description(raw_params["tags"]&.first))
173
+ progress_html = view_context.render_creative_progress(creative, select_mode: !!select_mode)
174
+
175
+ {
176
+ description_html: description_html,
177
+ progress_html: progress_html,
178
+ edit_icon_html: edit_icon_html,
179
+ edit_off_icon_html: edit_off_icon_html,
180
+ origin_link_html: origin_link_html_for(creative)
181
+ }
182
+ end
183
+
184
+ def inline_editor_payload_for(creative)
185
+ {
186
+ description_raw_html: creative.effective_description(nil, true),
187
+ progress: creative.progress,
188
+ origin_id: creative.origin_id
189
+ }
190
+ end
191
+
192
+ def children_container_payload(creative, filtered_children, child_level:, children_nodes:, expanded:, load_children_now:)
193
+ return nil unless filtered_children.any?
194
+
195
+ {
196
+ id: "creative-children-#{creative.id}",
197
+ expanded: expanded,
198
+ loaded: load_children_now,
199
+ load_url: view_context.collavre.children_creative_path(
200
+ creative,
201
+ level: child_level,
202
+ select_mode: select_mode ? 1 : 0
203
+ ),
204
+ level: child_level,
205
+ nodes: children_nodes
206
+ }
207
+ end
208
+
209
+ def edit_icon_html
210
+ @edit_icon_html ||= view_context.svg_tag("edit.svg", className: "icon-edit")
211
+ end
212
+
213
+ def edit_off_icon_html
214
+ @edit_off_icon_html ||= view_context.svg_tag("edit-off.svg", className: "icon-edit")
215
+ end
216
+
217
+ def origin_link_html_for(creative)
218
+ return unless creative.origin_id.present?
219
+
220
+ view_context.link_to(
221
+ view_context.collavre.creative_path(creative.origin),
222
+ class: "creative-origin-link creative-action-btn unstyled-link",
223
+ title: I18n.t("collavre.creatives.index.view_origin"),
224
+ aria: { label: I18n.t("collavre.creatives.index.view_origin") }
225
+ ) do
226
+ view_context.svg_tag("arrow-right.svg", class: "creative-origin-link-icon", width: 16, height: 16)
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,36 @@
1
+ module Collavre
2
+ module Creatives
3
+ class TreeFormatter
4
+ def format(creatives)
5
+ roots = Array(creatives)
6
+ lines = []
7
+
8
+ roots.each do |root|
9
+ format_node(root, 0, lines)
10
+ end
11
+
12
+ lines.join("\n")
13
+ end
14
+
15
+ private
16
+
17
+ def format_node(node, depth, lines)
18
+ indent = " " * (depth * 4)
19
+ desc = ActionController::Base.helpers.strip_tags(node.effective_description(nil, false))
20
+ progress = node.progress || 0.0
21
+
22
+ node_data = { id: node.id, progress: progress, desc: desc }
23
+ lines << "#{indent}- #{node_data.to_json}"
24
+
25
+ # We need to handle children. If the node is a new instance (in tests),
26
+ # children might depend on how it's set up.
27
+ # In real app, we use children association.
28
+ # To match the logic in views/controllers where we might want specific ordering:
29
+ # We'll validly assume `children` association is available.
30
+ node.children.each do |child|
31
+ format_node(child, depth + 1, lines)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,77 @@
1
+ module Collavre
2
+ class GeminiParentRecommender
3
+ def initialize(client: default_client)
4
+ @client = client
5
+ end
6
+
7
+ def recommend(creative)
8
+ user = creative.user
9
+
10
+ # 1. Gather all potential category candidates
11
+ # Candidates: All creatives user can write to + creative's current parent (even if not writable?)
12
+ # Original logic: Distinct creatives having children + parent.
13
+ # Note: Using `joins(:children)` filters only those that have children.
14
+
15
+ categories = Creative
16
+ .joins(:children)
17
+ .distinct
18
+ .where(origin_id: nil)
19
+ .select { |c| c.has_permission?(user, :write) }
20
+
21
+ parent = creative.parent
22
+ if parent&.has_permission?(user, :write)
23
+ categories << parent
24
+ end
25
+ categories = categories.uniq
26
+
27
+ # Rebuild tree in memory to avoid N+1 in TreeFormatter
28
+ children_map = categories.group_by(&:parent_id)
29
+ categories.each do |c|
30
+ # Manually set the children association target
31
+ c.association(:children).target = (children_map[c.id] || []).select { |child| child.has_permission?(user, :write) }
32
+ end
33
+ category_ids = categories.index_by(&:id)
34
+ top_level_categories = categories.reject { |c| category_ids.key?(c.parent_id) }
35
+
36
+ tree_text = Creatives::TreeFormatter.new.format(top_level_categories)
37
+
38
+ prompt = build_prompt(tree_text, ActionController::Base.helpers.strip_tags(creative.description).to_s)
39
+ Rails.logger.info("### prompt=#{prompt}")
40
+
41
+ response = @client.chat([ { role: :user, parts: [ { text: prompt } ] } ])
42
+ ids = parse_response(response)
43
+
44
+ # Reconstruct paths for result
45
+ ids.map do |id|
46
+ c = Creative.find_by(id: id)
47
+ next unless c
48
+ path = c.ancestors.reverse.map { |a| ActionController::Base.helpers.strip_tags(a.description) } + [ ActionController::Base.helpers.strip_tags(c.description) ]
49
+ { id: id, path: path.join(" > ") }
50
+ end.compact
51
+ end
52
+
53
+ private
54
+
55
+ def default_client
56
+ AiClient.new(
57
+ vendor: "google",
58
+ model: "gemini-2.5-flash",
59
+ system_prompt: nil
60
+ )
61
+ end
62
+
63
+ def build_prompt(tree_text, description)
64
+ "#{tree_text}\n\nGiven the above creative tree, which ids are the best parents for \"#{description}\"? " \
65
+ "Reply with up to 5 ids separated by commas in descending order of relevance."
66
+ end
67
+
68
+ def parse_response(content)
69
+ return [] if content.blank?
70
+
71
+ content.to_s.split(/[\s,]+/)
72
+ .filter_map { |value| Integer(value, exception: false) }
73
+ .reject(&:zero?)
74
+ .first(5)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,112 @@
1
+ module Collavre
2
+ module Github
3
+ class Client
4
+ def initialize(account)
5
+ @client = Octokit::Client.new(access_token: account.token)
6
+ @client.auto_paginate = true
7
+ end
8
+
9
+ def organizations
10
+ client.organizations
11
+ rescue Octokit::Error, Faraday::Error => e
12
+ Rails.logger.warn("GitHub organizations fetch failed: #{e.message}")
13
+ []
14
+ end
15
+
16
+ def repositories_for_authenticated_user
17
+ client.repos(nil, type: "all")
18
+ rescue Octokit::Error, Faraday::Error => e
19
+ Rails.logger.warn("GitHub user repos fetch failed: #{e.message}")
20
+ []
21
+ end
22
+
23
+ def repositories_for_organization(org)
24
+ client.org_repos(org, type: "all")
25
+ rescue Octokit::Error, Faraday::Error => e
26
+ Rails.logger.warn("GitHub org repos fetch failed: #{e.message}")
27
+ []
28
+ end
29
+
30
+ def pull_request_details(repo_full_name, number)
31
+ client.pull_request(repo_full_name, number)
32
+ rescue Octokit::Error, Faraday::Error => e
33
+ Rails.logger.warn("GitHub PR fetch failed: #{e.message}")
34
+ nil
35
+ end
36
+
37
+ def pull_request_commit_messages(repo_full_name, number)
38
+ client
39
+ .pull_request_commits(repo_full_name, number)
40
+ .map { |commit| commit.commit&.message }
41
+ .compact
42
+ rescue Octokit::Error, Faraday::Error => e
43
+ Rails.logger.warn("GitHub PR commits fetch failed: #{e.message}")
44
+ []
45
+ end
46
+
47
+ def pull_request_diff(repo_full_name, number)
48
+ files = client.pull_request_files(repo_full_name, number)
49
+ formatted = files.filter_map do |file|
50
+ next unless file.patch.present?
51
+
52
+ <<~DIFF.strip
53
+ diff --git a/#{file.filename} b/#{file.filename}
54
+ #{file.patch}
55
+ DIFF
56
+ end
57
+ formatted.join("\n\n").presence
58
+ rescue Octokit::Error, Faraday::Error => e
59
+ Rails.logger.warn("GitHub PR files fetch failed: #{e.message}")
60
+ nil
61
+ end
62
+
63
+ def repository_hooks(repo_full_name)
64
+ client.hooks(repo_full_name)
65
+ rescue Octokit::Error, Faraday::Error => e
66
+ Rails.logger.warn("GitHub hooks fetch failed for #{repo_full_name}: #{e.message}")
67
+ []
68
+ end
69
+
70
+ def create_repository_webhook(repo_full_name, url:, secret:, events:, content_type: "json")
71
+ client.create_hook(
72
+ repo_full_name,
73
+ "web",
74
+ {
75
+ url: url,
76
+ content_type: content_type,
77
+ secret: secret
78
+ },
79
+ {
80
+ events: events,
81
+ active: true
82
+ }
83
+ )
84
+ end
85
+
86
+ def update_repository_webhook(repo_full_name, hook_id, url:, secret:, events:, content_type: "json")
87
+ client.edit_hook(
88
+ repo_full_name,
89
+ hook_id,
90
+ "web",
91
+ {
92
+ url: url,
93
+ content_type: content_type,
94
+ secret: secret
95
+ },
96
+ {
97
+ events: events,
98
+ active: true
99
+ }
100
+ )
101
+ end
102
+
103
+ def delete_repository_webhook(repo_full_name, hook_id)
104
+ client.remove_hook(repo_full_name, hook_id)
105
+ end
106
+
107
+ private
108
+
109
+ attr_reader :client
110
+ end
111
+ end
112
+ end