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,19 @@
1
+ module Collavre
2
+ module SystemEvents
3
+ class Dispatcher
4
+ def self.dispatch(event_name, context)
5
+ new.dispatch(event_name, context)
6
+ end
7
+
8
+ def dispatch(event_name, context)
9
+ # Build context once to ensure consistency between Router and Job
10
+ enriched_context = Collavre::SystemEvents::ContextBuilder.new(context).build
11
+ agents = Router.new.route(event_name, enriched_context)
12
+
13
+ agents.each do |agent|
14
+ AiAgentJob.perform_later(agent.id, event_name, enriched_context)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,72 @@
1
+ module Collavre
2
+ module SystemEvents
3
+ class Router
4
+ def route(event_name, context)
5
+ # Build the context for Liquid
6
+ liquid_context = Collavre::SystemEvents::ContextBuilder.new(context).build
7
+ liquid_context["event_name"] = event_name
8
+
9
+ # Find all AI agents
10
+ agents = User.where.not(llm_vendor: nil)
11
+
12
+ matched_agents = []
13
+
14
+ agents.each do |agent|
15
+ next if agent.routing_expression.blank?
16
+
17
+ # Permission Check
18
+ # If agent is not searchable, it must have feedback permission on the creative
19
+ unless agent.searchable?
20
+ creative_id = context.dig("creative", "id") || context.dig(:creative, :id)
21
+ if creative_id
22
+ creative = Creative.find_by(id: creative_id)
23
+ if creative
24
+ # Check for feedback permission (which implies read access)
25
+ # has_permission? checks for the specific permission or higher
26
+ unless creative.has_permission?(agent, :feedback)
27
+ # Rails.logger.info "Agent #{agent.id} skipped: No feedback permission on Creative #{creative.id}"
28
+ next
29
+ end
30
+ else
31
+ # If creative ID is present but not found, skip for safety
32
+ next
33
+ end
34
+ else
35
+ # If no creative context, we might skip or allow depending on policy.
36
+ # Assuming 'chat.creative' implies creative context is required for this check.
37
+ # If it's a global event without creative, maybe searchable check isn't needed?
38
+ # But the user said "must have feedback permission on the chat.creative".
39
+ # If there is no creative, we can't check permission, so we should probably skip to be safe
40
+ # unless it's a purely global event. But for now, let's skip.
41
+ next
42
+ end
43
+ end
44
+
45
+ begin
46
+ # Add 'agent' to context so they can refer to themselves
47
+ agent_context = liquid_context.merge("agent" => agent.as_json(only: [ :id, :name, :email ]))
48
+
49
+ # Parse and evaluate the routing expression
50
+ # We wrap the expression in an if block to evaluate truthiness
51
+ expression = agent.routing_expression.strip
52
+ unless expression.start_with?("{%")
53
+ expression = "{% if #{expression} %}true{% endif %}"
54
+ end
55
+
56
+ template = Liquid::Template.parse(expression)
57
+ result = template.render(agent_context)
58
+
59
+ # Check if the result evaluates to "true" string or boolean true
60
+ if result.strip == "true"
61
+ matched_agents << agent
62
+ end
63
+ rescue StandardError => e
64
+ Rails.logger.error("Routing error for agent #{agent.id}: #{e.message}")
65
+ end
66
+ end
67
+
68
+ matched_agents
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,138 @@
1
+ module Collavre
2
+ require "sorbet-runtime"
3
+ require "rails_mcp_engine"
4
+ module Tools
5
+ class CreativeRetrievalService
6
+ extend T::Sig
7
+ extend ToolMeta
8
+
9
+ tool_name "creative_retrieval_service"
10
+ tool_description "Retrieve creatives by ID or query text. without query or ID, it will return root creatives. Returns a list of matching creatives with their details, supporting both hierarchical tree and flat list formats.\n\nA Creative is a content block that functions like a task, organized in a tree structure similar to a to-do list. You can navigate the tree at any level as a structured document, with progress automatically calculated to show what’s been completed.\n\ne.g.\n- When user say creative or Test creative, it means \"Test\" creative and it's children as a writing page.\n- Summary of Test creative? - you need to search \"Test\" creatives with level 3 or more and find the title is \"Test\" or similar and make summary of that."
11
+
12
+ tool_param :id, description: "The ID of the creative to retrieve."
13
+ tool_param :query, description: "Text to search for in creative descriptions."
14
+ tool_param :level, description: "Creative tree depth to return (default: 3).", required: false
15
+ tool_param :simple, description: "If true, returns a simplified flat list. If false (default), returns a tree structure with HTML.", required: false
16
+
17
+ sig { params(id: T.nilable(Integer), query: T.nilable(String), level: T.nilable(Integer), simple: T.nilable(T::Boolean)).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
18
+ def call(id: nil, query: nil, level: 3, simple: false)
19
+ level ||= 3
20
+ simple ||= false
21
+
22
+ # Ensure fresh permission cache for this tool execution
23
+ Current.creative_share_cache = nil if Current.respond_to?(:creative_share_cache=)
24
+
25
+ # Mock session and request setup
26
+ setup_mock_environment
27
+
28
+ controller = Collavre::CreativesController.new
29
+ setup_controller(controller)
30
+
31
+ if id.present?
32
+ # Get the creative details (show)
33
+ show_result = dispatch_request(controller, :show, id: id, format: :json)
34
+ return show_result if show_result.is_a?(Array) && show_result.first[:error]
35
+
36
+ # Get the children (index with id acts as parent filter)
37
+ index_result = dispatch_request(controller, :index, id: id, search: query, simple: simple, level: level, format: :json)
38
+ return index_result if index_result.is_a?(Array) && index_result.first[:error]
39
+
40
+ # Combine results
41
+ # show_result is expected to be a hash of the creative
42
+ # index_result is expected to be a list of children or simple list
43
+
44
+ # Parse show result
45
+ creative_details = JSON.parse(show_result[:body], symbolize_names: true)
46
+
47
+ # Parse index result
48
+ children_data = JSON.parse(index_result[:body], symbolize_names: true)
49
+ filtered_children = filter_result(children_data)
50
+
51
+ # Merge
52
+ # We construct a tree node for the parent, with the children attached
53
+ parent_node = filter_tree([ creative_details ]).first
54
+ parent_node[:children] = filtered_children
55
+
56
+ [ parent_node ]
57
+ else
58
+ # Normal index call
59
+ result = dispatch_request(controller, :index, search: query, simple: simple, level: level, format: :json)
60
+
61
+ if result[:status] == 200
62
+ parsed = JSON.parse(result[:body], symbolize_names: true)
63
+ filter_result(parsed)
64
+ else
65
+ [ { error: "Controller returned status #{result[:status]}", body: result[:body] } ]
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def setup_mock_environment
73
+ raise "Current.user is required" unless Current.user
74
+ unless Current.session
75
+ require "ostruct"
76
+ Current.session = OpenStruct.new(user: Current.user, persisted?: false)
77
+ end
78
+ end
79
+
80
+ def setup_controller(controller)
81
+ # Stub cookies
82
+ controller.define_singleton_method(:cookies) do
83
+ @mock_cookies ||= begin
84
+ jar = OpenStruct.new
85
+ def jar.signed; self; end
86
+ def jar.encrypted; self; end
87
+ def jar.[](key); nil; end
88
+ def jar.delete(key); nil; end
89
+ jar
90
+ end
91
+ end
92
+ end
93
+
94
+ def dispatch_request(controller, action, params)
95
+ env = Rack::MockRequest.env_for(
96
+ "/creatives",
97
+ method: "GET",
98
+ params: params.compact,
99
+ "HTTP_X_ORIGIN_SECRET" => ENV["ORIGIN_SHARED_SECRET"] # Internal call
100
+ )
101
+ controller.request = ActionDispatch::Request.new(env)
102
+ controller.response = ActionDispatch::Response.new
103
+ controller.process(action)
104
+
105
+ { status: controller.response.status, body: controller.response.body }
106
+ end
107
+
108
+ def filter_result(result)
109
+ if result.is_a?(Array)
110
+ # Simple mode
111
+ result.map { |item| item.slice(:id, :description, :progress) }
112
+ elsif result.is_a?(Hash) && result[:creatives].is_a?(Array)
113
+ # Normal mode (Tree)
114
+ filter_tree(result[:creatives])
115
+ else
116
+ []
117
+ end
118
+ end
119
+
120
+ def filter_tree(nodes)
121
+ nodes.map do |node|
122
+ description = if node.dig(:templates, :description_html)
123
+ Rails::Html::FullSanitizer.new.sanitize(node.dig(:templates, :description_html))
124
+ else
125
+ node[:description]
126
+ end
127
+
128
+ {
129
+ id: node[:id],
130
+ description: description&.strip,
131
+ progress: node.dig(:inline_editor_payload, :progress) || node[:progress],
132
+ children: node.dig(:children_container, :nodes) ? filter_tree(node.dig(:children_container, :nodes)) : []
133
+ }
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,4 @@
1
+ <div class="tab-list">
2
+ <%= link_to t('admin.tabs.system'), main_app.admin_path, class: "tab-button #{'active' if controller_name == 'settings'}" %>
3
+ <%= link_to t('admin.tabs.users'), collavre.users_path, class: "tab-button #{'active' if controller_name == 'users'}" %>
4
+ </div>
@@ -0,0 +1,23 @@
1
+ <%= turbo_frame_tag "activity_log_details_#{comment.id}" do %>
2
+ <% if activity_logs.any? %>
3
+ <div class="activity-log-list">
4
+ <% activity_logs.each do |log| %>
5
+ <div class="activity-log-item">
6
+ <details>
7
+ <summary class="activity-log-summary">
8
+ <span class="activity-name"><%= log.activity %></span>
9
+ <span class="activity-time"><%= time_ago_in_words(log.created_at) %> ago</span>
10
+ </summary>
11
+ <div class="activity-log-body">
12
+ <pre class="activity-log-yaml"><code><%= log.log.to_yaml.sub(/^---\n/, '') %></code></pre>
13
+ </div>
14
+ </details>
15
+ </div>
16
+ <% end %>
17
+ </div>
18
+ <% else %>
19
+ <div class="activity-log-empty">
20
+ No activity logs available.
21
+ </div>
22
+ <% end %>
23
+ <% end %>
@@ -0,0 +1,147 @@
1
+ <% comment_topic = comment.topic %>
2
+ <% current_topic_id = local_assigns[:current_topic_id] %>
3
+ <div class="comment-item" id="<%= dom_id(comment) %>" data-controller="comment" data-user-id="<%= comment.user&.id %>" data-comment-id="<%= comment.id %>" data-topic-id="<%= comment_topic&.id %>" data-creative-id="<%= comment.creative_id %>">
4
+ <div class="comment-select">
5
+ <input type="checkbox"
6
+ class="comment-select-checkbox"
7
+ value="<%= comment.id %>"
8
+ aria-label="<%= t('collavre.comments.select_label') %>" />
9
+ </div>
10
+ <div>
11
+ <%= render AvatarComponent.new(user: comment.user, size: 20, classes: 'avatar comment-avatar') %>
12
+ <% system_prefix = "#{t('collavre.comments.system_user')}:" %>
13
+ <% display_name =
14
+ if comment.user.present?
15
+ comment.user.display_name
16
+ elsif comment.content.to_s.strip.start_with?(system_prefix)
17
+ t('collavre.comments.system_user')
18
+ else
19
+ t('collavre.comments.gemini')
20
+ end %>
21
+ <% timestamp = comment.created_at.in_time_zone %>
22
+ <strong><%= display_name %></strong>
23
+ <span>
24
+ · <%= content_tag(
25
+ :time,
26
+ t('datetime.ago', time: time_ago_in_words(timestamp)),
27
+ title: l(timestamp, format: :chat_timestamp),
28
+ datetime: timestamp.iso8601
29
+ ) %>
30
+ </span>
31
+ <span id="read_receipts_comment_<%= comment.id %>" class="read-receipt-wrapper">
32
+ <% users_read = defined?(read_by_users) ? read_by_users : (defined?(read_receipts) ? read_receipts[comment.id] : nil) %>
33
+ <% present_user_ids = defined?(present_user_ids) ? present_user_ids : nil %>
34
+ <%= render "collavre/comments/read_receipts", read_by_users: users_read, present_user_ids: present_user_ids unless comment.private? %>
35
+ </span>
36
+ <% if comment.private? %>
37
+ <span class="private-label">[<%= t('collavre.comments.private') %>]</span>
38
+ <% end %>
39
+ <% if comment.action_executed_at.present? %>
40
+ <span class="comment-status-label approved-label">[<%= t('collavre.comments.approved_label') %>]</span>
41
+ <% end %>
42
+ <% can_convert_comment = comment.user == Current.user || comment.creative.has_permission?(Current.user, :admin) %>
43
+ </div>
44
+ <div class="comment-action-container">
45
+ <button class="add-reaction-btn" type="button" data-action="click->comment#triggerReactionPicker" title="<%= t('collavre.comments.add_reaction') %>">
46
+ <span class="grayscale-emoji">☺</span>
47
+ </button>
48
+
49
+ <button class="<%= ['convert-comment-btn', ('comment-owner-only' unless can_convert_comment)].compact.join(' ') %>" data-comment-target="ownerButton" data-comment-id="<%= comment.id %>" title="<%= t('collavre.comments.convert_to_creative') %>">
50
+ <%= t('collavre.comments.convert_button') %>
51
+ </button>
52
+ <% can_approve = comment.approval_status(Current.user) == :ok %>
53
+ <% if can_approve %>
54
+ <button class="approve-comment-btn" data-comment-id="<%= comment.id %>" title="<%= t('collavre.comments.approve_button') %>">
55
+ <%= t('collavre.comments.approve_button') %>
56
+ </button>
57
+ <% end %>
58
+ <button class="edit-comment-btn comment-owner-only" data-comment-target="ownerButton" data-comment-id="<%= comment.id %>" data-comment-content="<%= comment.content %>" data-comment-private="<%= comment.private? %>" title="<%= t('collavre.comments.update_comment') %>">
59
+ <%= t('collavre.comments.edit_button') %>
60
+ </button>
61
+ <button class="copy-comment-link-btn" data-comment-id="<%= comment.id %>" data-comment-url="<%= collavre.creative_comment_url(comment.creative, comment, Rails.application.config.action_mailer.default_url_options) %>" title="<%= t('collavre.comments.copy_link_button') %>">
62
+ <%= t('collavre.comments.copy_link_button') %>
63
+ </button>
64
+ <% can_delete = comment.user == Current.user || comment.creative.user == Current.user || comment.creative.has_permission?(Current.user, :admin) %>
65
+ <% if can_delete %>
66
+ <button class="delete-comment-btn" data-comment-id="<%= comment.id %>" title="<%= t('collavre.comments.delete') %>">
67
+ <%= t('collavre.comments.delete_button') %>
68
+ </button>
69
+ <% end %>
70
+ </div>
71
+ <div class="comment-content"><%= comment.content %></div>
72
+
73
+
74
+ <% if comment.images.attached? %>
75
+ <div class="comment-attachments">
76
+ <% comment.images.each do |image| %>
77
+ <% preview_image = image.variable? ? image.variant(resize_to_limit: [ 800, 800 ]) : image %>
78
+ <% preview_image_path =
79
+ if preview_image.respond_to?(:processed)
80
+ main_app.rails_representation_path(preview_image, only_path: true)
81
+ else
82
+ main_app.rails_blob_path(preview_image, only_path: true)
83
+ end %>
84
+ <%= link_to main_app.rails_blob_path(image, only_path: true), target: '_blank', rel: 'noopener', class: 'comment-attachment-link' do %>
85
+ <%= image_tag preview_image_path, class: 'comment-attachment-image', alt: '', loading: 'lazy' %>
86
+ <% end %>
87
+ <% end %>
88
+ </div>
89
+ <% end %>
90
+ <% reaction_emojis = [ "👍", "🎉", "❤️", "😂", "😮", "😢", "😡" ] %>
91
+ <% reactions = comment.comment_reactions.to_a %>
92
+ <% reaction_groups = reactions.group_by(&:emoji) %>
93
+ <% current_user_id = Current.user&.id %>
94
+ <% reacted_emojis = reactions.filter { |reaction| reaction.user_id == current_user_id }.map(&:emoji).to_set %>
95
+ <% if reaction_groups.any? %>
96
+ <div class="comment-reactions">
97
+ <div class="comment-reaction-list">
98
+ <% reaction_groups.sort_by { |emoji, _| emoji }.each do |emoji, grouped| %>
99
+ <% count = grouped.size %>
100
+ <% count_class = count > 1 ? "comment-reaction-count is-visible" : "comment-reaction-count" %>
101
+ <button class="comment-reaction <%= 'reacted' if reacted_emojis.include?(emoji) %>" type="button" data-action="click->comment#toggleReaction" data-emoji="<%= emoji %>" data-reacted="<%= reacted_emojis.include?(emoji) %>">
102
+ <span class="comment-reaction-emoji"><%= emoji %></span>
103
+ <span class="<%= count_class %>"><%= count %></span>
104
+ </button>
105
+ <% end %>
106
+ </div>
107
+ </div>
108
+ <% end %>
109
+
110
+ <% if comment_topic.present? && current_topic_id.blank? %>
111
+ <div class="comment-topic-link">
112
+ <a href="#" class="comment-topic-switch" data-topic-id="<%= comment_topic.id %>">#<%= comment_topic.name %></a>
113
+ </div>
114
+ <% end %>
115
+ <% if comment.action.present? %>
116
+ <div class="comment-action-block" data-comment-id="<%= comment.id %>">
117
+ <details class="comment-action-details">
118
+ <summary class="comment-action-summary"><%= t("collavre.comments.action_summary") %></summary>
119
+ <div class="comment-action-body">
120
+ <pre class="comment-action-json" data-comment-action-json><%= formatted_comment_action(comment) %></pre>
121
+ <% if can_approve && comment.action_executed_at.blank? %>
122
+ <button class="edit-comment-action-btn" type="button" data-comment-id="<%= comment.id %>"><%= t("collavre.comments.edit_action_button") %></button>
123
+ <form class="comment-action-edit-form" data-comment-id="<%= comment.id %>" style="display:none;">
124
+ <textarea class="comment-action-edit-textarea" name="comment[action]" rows="8"><%= formatted_comment_action(comment) %></textarea>
125
+ <div class="comment-action-edit-buttons">
126
+ <button class="cancel-comment-action-edit-btn" type="button"><%= t("app.cancel") %></button>
127
+ <button class="save-comment-action-btn" type="submit"><%= t("app.save") %></button>
128
+ </div>
129
+ </form>
130
+ <% end %>
131
+ </div>
132
+ </details>
133
+ </div>
134
+ <% end %>
135
+ <% if comment.activity_logs.exists? %>
136
+ <div class="comment-activity-log-block">
137
+ <details>
138
+ <summary><%= t("collavre.comments.activity_logs_summary") %></summary>
139
+ <%= turbo_frame_tag "activity_log_details_#{comment.id}", src: collavre.creative_comment_activity_log_path(comment.creative, comment), loading: "lazy" do %>
140
+ Loading...
141
+ <% end %>
142
+ </details>
143
+ </div>
144
+ <% end %>
145
+
146
+
147
+ </div>
@@ -0,0 +1,46 @@
1
+
2
+ <div id="comments-popup" data-controller="comments--popup comments--list comments--form comments--presence comments--mention-menu comments--topics" class="popup-box"
3
+ data-loading-text="<%= t('app.loading') %>"
4
+ data-delete-confirm-text="<%= t("collavre.comments.delete_confirm") %>"
5
+ data-update-comment-text="<%= t('collavre.comments.update_comment') %>"
6
+ data-convert-confirm-text="<%= t('collavre.comments.convert_confirm') %>"
7
+ data-copy-link-success-text="<%= t('collavre.comments.copy_link_success') %>"
8
+ data-copy-link-error-text="<%= t('collavre.comments.copy_link_error') %>"
9
+ data-approve-success-text="<%= t('collavre.comments.approve_success') %>"
10
+ data-approve-error-text="<%= t('collavre.comments.approve_error') %>"
11
+ data-action-update-success-text="<%= t('collavre.comments.action_update_success') %>"
12
+ data-action-update-error-text="<%= t('collavre.comments.action_update_error') %>"
13
+ data-search-empty-text="<%= t('collavre.comments.search_empty_prompt') %>"
14
+ data-speech-unavailable-text="<%= t('collavre.comments.speech_unavailable') %>"
15
+ data-voice-start-text="<%= t('collavre.comments.voice_button') %>"
16
+ data-voice-stop-text="<%= t('collavre.comments.voice_stop') %>"
17
+ data-move-no-selection-text="<%= t('collavre.comments.move_no_selection') %>"
18
+ data-move-error-text="<%= t('collavre.comments.move_error') %>">
19
+ <div class="resize-handle resize-handle-left" data-comments--popup-target="leftHandle"></div>
20
+ <div class="resize-handle resize-handle-right" data-comments--popup-target="rightHandle"></div>
21
+ <button id="close-comments-btn" data-comments--popup-target="closeButton" class="popup-close-btn">&times;</button>
22
+ <h3 id="comments-popup-title" data-comments--popup-target="title"><%= t('collavre.comments.comments') %></h3>
23
+ <div id="comment-participants" data-comments--presence-target="participants" data-comments--mention-menu-target="participants"></div>
24
+ <div id="comment-topics" data-comments--topics-target="list" class="comment-topics-list"
25
+ data-confirm-delete-text="<%= t('collavre.topics.delete_confirm') %>"
26
+ data-new-topic-placeholder="<%= t('collavre.topics.new_placeholder') %>"></div>
27
+ <%= render UserMentionMenuComponent.new(menu_id: 'mention-menu') %>
28
+ <div id="comments-list" data-comments--popup-target="list" data-comments--list-target="list"><%= t('app.loading') %></div>
29
+ <div id="typing-indicator" data-comments--presence-target="typingIndicator"></div>
30
+ <form id="new-comment-form" data-comments--popup-target="form" data-comments--form-target="form" style="display:none;">
31
+ <textarea class="shared-input-surface" name="comment[content]" data-comments--form-target="textarea" data-comments--presence-target="textarea" data-comments--mention-menu-target="textarea" rows="2" enterkeyhint="send"></textarea>
32
+ <div class="comment-bottom">
33
+ <input type="file" id="comment-images" name="comment[images][]" accept="image/*" multiple data-comments--form-target="imageInput" style="display:none;" />
34
+ <label style="height: 22px"><input type="checkbox" id="comment-private" data-comments--form-target="privateCheckbox" data-comments--presence-target="privateCheckbox" name="comment[private]" /> <%= t('collavre.comments.private') %></label>
35
+ <div class="comment-actions">
36
+ <button class="creative-action-btn" id="attach-image-btn" data-comments--form-target="imageButton" type="button"><%= t('collavre.comments.image_button') %></button>
37
+ <button class="creative-action-btn" id="voice-comments-btn" data-comments--form-target="voiceButton" type="button" data-voice-state="idle"><%= t('collavre.comments.voice_button') %></button>
38
+ <button class="creative-action-btn" id="search-comments-btn" data-comments--form-target="searchButton" type="button"><%= t('collavre.comments.search_button') %></button>
39
+ <button class="creative-action-btn" id="move-comments-btn" data-comments--form-target="moveButton" type="button" disabled><%= t('collavre.comments.move_button') %></button>
40
+ <button class="creative-action-btn" id="cancel-edit-btn" data-comments--form-target="cancel" type="button" style="display:none;"><%= t('app.cancel') %></button>
41
+ <button class="creative-action-btn" type="submit" data-comments--form-target="submit"><%= svg_tag 'send.svg', class: 'send-icon' %></button>
42
+ </div>
43
+ <div class="comment-attachment-list" data-comments--form-target="attachmentList" aria-live="polite"></div>
44
+ </div>
45
+ </form>
46
+ </div>
@@ -0,0 +1,10 @@
1
+ <%= turbo_stream_from [creative, :comments] %>
2
+ <% if comments.any? %>
3
+ <% current_topic_id = local_assigns[:current_topic_id] %>
4
+ <% comments.each do |comment| %>
5
+ <%= render partial: 'collavre/comments/comment', locals: { comment: comment, read_by_users: read_receipts&.[](comment.id), present_user_ids: present_user_ids, current_topic_id: current_topic_id } %>
6
+ <% end %>
7
+ <% else %>
8
+ <div id="no-comments"><%= t('collavre.comments.no_comments') %></div>
9
+ <% end %>
10
+
@@ -0,0 +1,8 @@
1
+ <div id="comment-participants">
2
+ <% accessible_users = [creative.user].compact + creative.all_shared_users(:feedback).map(&:user) %>
3
+ <% accessible_users.uniq.each do |u| %>
4
+ <% classes = 'avatar comment-presence-avatar' %>
5
+ <% classes += ' inactive' unless present_ids.include?(u.id) %>
6
+ <%= render AvatarComponent.new(user: u, size: 20, classes: classes, data: { email: u.email }) %>
7
+ <% end %>
8
+ </div>
@@ -0,0 +1,15 @@
1
+ <div id="global-reaction-picker"
2
+ class="comment-reaction-picker"
3
+ hidden
4
+ data-controller="reaction-picker"
5
+ data-action="click@window->reaction-picker#handleClickOutside"
6
+ style="position: fixed; z-index: 9999;">
7
+ <% ["👍", "🎉", "❤️", "😂", "😮", "😢", "😡", "✅", "👌", "👀"].each do |emoji| %>
8
+ <button class="comment-reaction-picker-emoji"
9
+ type="button"
10
+ data-action="click->reaction-picker#select"
11
+ data-emoji="<%= emoji %>">
12
+ <%= emoji %>
13
+ </button>
14
+ <% end %>
15
+ </div>
@@ -0,0 +1,19 @@
1
+ <% if defined?(read_by_users) && read_by_users.present? %>
2
+ <div class="read-receipt-container">
3
+ <div class="read-receipt-avatars">
4
+ <% read_by_users.each do |user| %>
5
+ <% present_ids = defined?(present_user_ids) ? present_user_ids : nil %>
6
+ <% classes = ['avatar', 'comment-presence-avatar'] %>
7
+ <% classes << 'inactive' if present_ids && !present_ids.include?(user.id) %>
8
+ <div class="read-receipt-avatar" title="<%= t('collavre.comments.read_by', name: user.display_name) %>">
9
+ <%= render AvatarComponent.new(
10
+ user: user,
11
+ size: 16,
12
+ classes: classes.join(' '),
13
+ data: { user_id: user.id }
14
+ ) %>
15
+ </div>
16
+ <% end %>
17
+ </div>
18
+ </div>
19
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <% if @parent_creative %>
2
+ <%= button_tag(
3
+ type: 'button',
4
+ class: 'popup-menu-item add-creative-btn',
5
+ data: { parent_id: @parent_creative.id },
6
+ aria: { label: t('collavre.creatives.index.new_creative') },
7
+ title: t('collavre.creatives.index.new_creative_shortcut')
8
+ ) do %>
9
+ <span aria-hidden="true"><%= svg_tag 'add.svg', class: 'icon-add', width: 16, height: 16 %></span>
10
+ <% end %>
11
+ <% else %>
12
+ <%= button_tag(
13
+ type: 'button',
14
+ class: 'popup-menu-item new-root-creative-btn',
15
+ aria: { label: t('collavre.creatives.index.new_creative') },
16
+ title: t('collavre.creatives.index.new_creative_shortcut')
17
+ ) do %>
18
+ <span aria-hidden="true"><%= svg_tag 'add.svg', class: 'icon-add', width: 16, height: 16 %></span>
19
+ <% end %>
20
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <%= render PopupMenuComponent.new(
2
+ button_content: t('collavre.creatives.index.delete'),
3
+ button_classes: ["danger-link", local_assigns[:extra_button_classes]].compact.join(" "),
4
+ menu_id: 'delete-options-popup-' + [local_assigns[:extra_button_classes]].compact.join('-')) do %>
5
+ <%= button_to t('collavre.creatives.index.delete_only_this'), @parent_creative, method: :delete,
6
+ form: { data: { turbo_confirm: t('collavre.creatives.index.are_you_sure_delete_only_this') } },
7
+ params: {}, class: 'danger-link popup-menu-item' %>
8
+ <%= button_to t('collavre.creatives.index.delete_with_children'), @parent_creative, method: :delete,
9
+ params: { delete_with_children: true },
10
+ form: { data: { turbo_confirm: t('collavre.creatives.index.are_you_sure_delete_with_children') } },
11
+ class: 'danger-link' %>
12
+ <% end %>
@@ -0,0 +1,77 @@
1
+ <div id="github-integration-modal"
2
+ data-success-message="<%= t('collavre.creatives.index.github_integration_saved', default: 'Github integration saved successfully.') %>"
3
+ data-login-required="<%= t('collavre.creatives.index.github_integration_login_required', default: 'Sign in with your Github account to start the integration.') %>"
4
+ data-no-creative="<%= t('collavre.creatives.index.github_integration_missing_creative', default: 'No Creative selected for integration.') %>"
5
+ data-webhook-url-label="<%= t('collavre.creatives.index.github_integration_webhook_url_label', default: 'Webhook URL') %>"
6
+ data-webhook-secret-label="<%= t('collavre.creatives.index.github_integration_webhook_secret_label', default: 'Webhook secret') %>"
7
+ data-existing-message="<%= t('collavre.creatives.index.github_integration_existing_message', default: 'You\'re already connected to the repositories below.') %>"
8
+ data-delete-confirm="<%= t('collavre.creatives.index.github_integration_delete_confirm', default: 'Do you want to remove the Github integration?') %>"
9
+ data-delete-success="<%= t('collavre.creatives.index.github_integration_delete_success', default: 'Github integration removed successfully.') %>"
10
+ data-delete-error="<%= t('collavre.creatives.index.github_integration_delete_error', default: 'Failed to remove the Github integration.') %>"
11
+ data-delete-button-label="<%= t('collavre.creatives.index.github_integration_delete_button', default: 'Remove integration') %>"
12
+ data-delete-select-warning="<%= t('collavre.creatives.index.github_integration_delete_select_warning', default: '삭제할 Repository를 선택하세요.') %>"
13
+ style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1000;align-items:center;justify-content:center;">
14
+ <div class="popup-box" style="min-width:360px;max-width:90vw;">
15
+ <button type="button" id="close-github-modal" class="popup-close-btn">&times;</button>
16
+ <h2><%= t('collavre.creatives.index.github_integration_title', default: 'Configure Github integration') %></h2>
17
+ <p id="github-integration-status" class="github-modal-status"></p>
18
+
19
+ <div class="github-wizard-step" id="github-step-connect">
20
+ <p id="github-connect-message" class="github-modal-subtext"><%= t('collavre.creatives.index.github_integration_connect', default: 'Sign in with your Github account to start linking.') %></p>
21
+ <form id="github-login-form" action="/auth/github" method="post" target="github-auth-window" style="display:none;">
22
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
23
+ </form>
24
+ <button type="button" id="github-login-btn" class="btn btn-primary" data-window-width="620" data-window-height="720">
25
+ <%= t('collavre.creatives.index.github_login_button', default: 'Sign in with Github') %>
26
+ </button>
27
+ <div id="github-existing-connections" style="display:none;margin-top:1.25em;">
28
+ <p style="margin-bottom:0.5em;"><%= t('collavre.creatives.index.github_integration_existing_intro', default: '이미 연동된 Repository 목록입니다:') %></p>
29
+ <ul id="github-existing-repo-list" style="padding-left:1.2em;margin-bottom:0.75em;color:var(--color-text);"></ul>
30
+ <button type="button" id="github-delete-btn" class="btn btn-danger" style="display:none;">
31
+ <%= t('collavre.creatives.index.github_integration_delete_button', default: '연동 삭제') %>
32
+ </button>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="github-wizard-step" id="github-step-organization" style="display:none;">
37
+ <p class="github-modal-subtext"><%= t('collavre.creatives.index.github_integration_choose_org', default: 'Select the organization that owns the repositories.') %></p>
38
+ <div id="github-organization-list" class="github-list github-modal-list-box" style="max-height:200px;overflow:auto;"></div>
39
+ </div>
40
+
41
+ <div class="github-wizard-step" id="github-step-repositories" style="display:none;">
42
+ <p class="github-modal-subtext"><%= t('collavre.creatives.index.github_integration_choose_repo', default: 'Select repositories to link. You can choose multiple.') %></p>
43
+ <div id="github-repository-list" class="github-list github-modal-list-box" style="max-height:240px;overflow:auto;"></div>
44
+ </div>
45
+
46
+ <div class="github-wizard-step" id="github-step-summary" style="display:none;">
47
+ <p class="github-modal-subtext"><%= t('collavre.creatives.index.github_integration_summary', default: 'The following repositories will be linked to this Creative:') %></p>
48
+ <p id="github-webhook-instructions" class="github-modal-subtext" style="display:none;margin-bottom:0.5em;"><%= t('collavre.creatives.index.github_integration_webhook_instructions', default: 'Configure each repository with the webhook details below.') %></p>
49
+ <ul id="github-selected-repos" style="padding-left:1.2em;color:var(--color-text);"></ul>
50
+ <p id="github-summary-empty" class="github-modal-empty" style="display:none;"><%= t('collavre.creatives.index.github_integration_summary_empty', default: 'No repositories selected.') %></p>
51
+ </div>
52
+
53
+ <div class="github-wizard-step" id="github-step-prompt" style="display:none;">
54
+ <p class="github-modal-subtext"><%= t('collavre.creatives.index.github_integration_prompt_title', default: 'Review the Gemini analysis prompt and edit it if needed.') %></p>
55
+ <textarea id="github-gemini-prompt" style="width:100%;min-height:220px;padding:0.75em;border:1px solid var(--color-border);border-radius:4px;font-family:monospace;font-size:0.95em;"></textarea>
56
+ <p class="github-modal-subtext" style="margin-top:0.75em;font-size:0.9em;">
57
+ <%= t('collavre.creatives.index.github_integration_prompt_help', default: 'Use the placeholders below to inject runtime details as needed:') %>
58
+ <code>#{pr_title}</code>,
59
+ <code>#{pr_body}</code>,
60
+ <code>#{commit_messages}</code>,
61
+ <code>#{diff}</code>,
62
+ <code>#{creative_tree}</code>,
63
+ <code>#{language_instructions}</code>
64
+ </p>
65
+ </div>
66
+
67
+ <div id="github-wizard-error" style="display:none;margin:0.5em 0;color:#c0392b;font-weight:bold;"></div>
68
+
69
+ <div class="github-wizard-footer" style="display:flex;justify-content:space-between;gap:0.5em;margin-top:1.5em;">
70
+ <button type="button" id="github-prev-btn" class="btn btn-secondary" style="display:none;"><%= t('app.previous', default: 'Previous') %></button>
71
+ <div style="margin-left:auto;display:flex;gap:0.5em;">
72
+ <button type="button" id="github-next-btn" class="btn btn-primary" style="display:none;"><%= t('app.next', default: 'Next') %></button>
73
+ <button type="button" id="github-finish-btn" class="btn btn-primary" style="display:none;"><%= t('app.finish', default: 'Finish') %></button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>