collavre 0.5.0 → 0.7.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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comment_versions.css +76 -0
  3. data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
  4. data/app/assets/stylesheets/collavre/creatives.css +73 -1
  5. data/app/assets/stylesheets/collavre/org_chart.css +319 -0
  6. data/app/assets/stylesheets/collavre/popup.css +68 -1
  7. data/app/controllers/collavre/application_controller.rb +13 -0
  8. data/app/controllers/collavre/comments/versions_controller.rb +82 -0
  9. data/app/controllers/collavre/comments_controller.rb +14 -153
  10. data/app/controllers/collavre/concerns/exportable.rb +30 -0
  11. data/app/controllers/collavre/concerns/shareable.rb +28 -0
  12. data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
  13. data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
  14. data/app/controllers/collavre/creative_imports_controller.rb +6 -0
  15. data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
  16. data/app/controllers/collavre/creative_plans_controller.rb +1 -1
  17. data/app/controllers/collavre/creative_shares_controller.rb +84 -14
  18. data/app/controllers/collavre/creatives_controller.rb +70 -194
  19. data/app/controllers/collavre/google_auth_controller.rb +3 -0
  20. data/app/controllers/collavre/invites_controller.rb +2 -1
  21. data/app/controllers/collavre/sessions_controller.rb +3 -0
  22. data/app/controllers/collavre/topics_controller.rb +39 -2
  23. data/app/controllers/collavre/users_controller.rb +5 -404
  24. data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
  25. data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
  26. data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
  27. data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
  28. data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
  29. data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
  30. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
  31. data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
  32. data/app/helpers/collavre/application_helper.rb +1 -0
  33. data/app/helpers/collavre/creatives_helper.rb +12 -9
  34. data/app/helpers/collavre/navigation_helper.rb +1 -1
  35. data/app/javascript/collavre.js +0 -1
  36. data/app/javascript/controllers/comment_controller.js +33 -70
  37. data/app/javascript/controllers/comment_version_controller.js +164 -0
  38. data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
  39. data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
  40. data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
  41. data/app/javascript/controllers/comments/contexts_controller.js +363 -0
  42. data/app/javascript/controllers/comments/form_controller.js +304 -13
  43. data/app/javascript/controllers/comments/list_controller.js +151 -62
  44. data/app/javascript/controllers/comments/popup_controller.js +66 -38
  45. data/app/javascript/controllers/comments/presence_controller.js +2 -10
  46. data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
  47. data/app/javascript/controllers/comments/topics_controller.js +34 -10
  48. data/app/javascript/controllers/index.js +15 -1
  49. data/app/javascript/controllers/org_chart_controller.js +46 -0
  50. data/app/javascript/controllers/share_modal_controller.js +369 -0
  51. data/app/javascript/controllers/topic_search_controller.js +103 -0
  52. data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
  53. data/app/javascript/lib/api/creatives.js +12 -0
  54. data/app/javascript/lib/api/csrf_fetch.js +35 -0
  55. data/app/javascript/lib/api/drag_drop.js +17 -0
  56. data/app/javascript/modules/command_menu.js +40 -0
  57. data/app/javascript/modules/creative_row_editor.js +88 -0
  58. data/app/javascript/modules/slide_view.js +2 -1
  59. data/app/jobs/collavre/ai_agent_job.rb +42 -30
  60. data/app/jobs/collavre/compress_job.rb +92 -0
  61. data/app/models/collavre/comment.rb +36 -1
  62. data/app/models/collavre/comment_version.rb +15 -0
  63. data/app/models/collavre/creative/describable.rb +1 -1
  64. data/app/models/collavre/creative.rb +51 -0
  65. data/app/models/collavre/task.rb +30 -2
  66. data/app/models/collavre/user.rb +20 -3
  67. data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
  68. data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
  69. data/app/services/collavre/ai_agent/message_builder.rb +85 -6
  70. data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
  71. data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
  72. data/app/services/collavre/ai_agent/review_handler.rb +18 -1
  73. data/app/services/collavre/ai_agent_service.rb +130 -183
  74. data/app/services/collavre/ai_client.rb +6 -0
  75. data/app/services/collavre/auto_theme_generator.rb +1 -1
  76. data/app/services/collavre/command_menu_service.rb +19 -0
  77. data/app/services/collavre/comments/command_processor.rb +3 -1
  78. data/app/services/collavre/comments/compress_command.rb +75 -0
  79. data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
  80. data/app/services/collavre/comments/work_command.rb +161 -0
  81. data/app/services/collavre/comments/workflow_executor.rb +276 -0
  82. data/app/services/collavre/creatives/plan_tagger.rb +14 -3
  83. data/app/services/collavre/creatives/tree_formatter.rb +53 -13
  84. data/app/services/collavre/gemini_parent_recommender.rb +4 -4
  85. data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
  86. data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
  87. data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
  88. data/app/services/collavre/orchestration/scheduler.rb +3 -2
  89. data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
  90. data/app/services/collavre/system_events/dispatcher.rb +9 -0
  91. data/app/services/collavre/tools/creative_create_service.rb +1 -8
  92. data/app/services/collavre/tools/creative_import_service.rb +46 -0
  93. data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
  94. data/app/services/collavre/tools/creative_update_service.rb +1 -8
  95. data/app/services/collavre/tools/cron_list_service.rb +1 -1
  96. data/app/services/collavre/tools/description_normalizable.rb +16 -0
  97. data/app/views/collavre/comments/_comment.html.erb +25 -8
  98. data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
  99. data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
  100. data/app/views/collavre/creatives/_share_button.html.erb +4 -1
  101. data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
  102. data/app/views/collavre/creatives/index.html.erb +5 -5
  103. data/app/views/collavre/creatives/slide_view.html.erb +1 -1
  104. data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
  105. data/app/views/collavre/users/_org_chart.html.erb +68 -0
  106. data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
  107. data/app/views/collavre/users/new_ai.html.erb +9 -0
  108. data/app/views/collavre/users/show.html.erb +32 -8
  109. data/config/locales/comments.en.yml +57 -2
  110. data/config/locales/comments.ko.yml +57 -2
  111. data/config/locales/contacts.en.yml +31 -0
  112. data/config/locales/contacts.ko.yml +31 -0
  113. data/config/locales/contexts.en.yml +8 -0
  114. data/config/locales/contexts.ko.yml +8 -0
  115. data/config/locales/creatives.en.yml +6 -0
  116. data/config/locales/creatives.ko.yml +6 -0
  117. data/config/locales/users.en.yml +1 -0
  118. data/config/locales/users.ko.yml +1 -0
  119. data/config/routes.rb +14 -1
  120. data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
  121. data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
  122. data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
  123. data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
  124. data/lib/collavre/version.rb +1 -1
  125. metadata +47 -10
  126. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
  127. data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
  128. data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
  129. data/app/javascript/modules/share_modal.js +0 -76
  130. data/app/javascript/modules/share_user_popup.js +0 -77
  131. data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
  132. data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
  133. data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
@@ -0,0 +1,363 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["list", "toggleButton"]
5
+
6
+ connect() {
7
+ this.contexts = []
8
+ this.canManage = false
9
+ this.draggingContextId = null
10
+ this.listVisible = false
11
+ }
12
+
13
+ get creativeId() {
14
+ return this.element.closest('#comments-popup')?.dataset?.creativeId
15
+ }
16
+
17
+ async onPopupOpened({ creativeId }) {
18
+ this._hasBeenManuallyToggled = false
19
+ this.listVisible = false
20
+ this._updateListVisibility()
21
+ await this.loadContexts()
22
+ }
23
+
24
+ onPopupClosed() {
25
+ this.contexts = []
26
+ this.canManage = false
27
+ if (this.hasListTarget) {
28
+ this.listTarget.innerHTML = ''
29
+ }
30
+ }
31
+
32
+ async loadContexts() {
33
+ const creativeId = this.creativeId
34
+ if (!creativeId) return
35
+
36
+ try {
37
+ const response = await fetch(`/creatives/${creativeId}/contexts`)
38
+ if (response.ok) {
39
+ const data = await response.json()
40
+ this.contexts = data.contexts || []
41
+ this.canManage = data.can_manage || false
42
+ this._selfContextDisabled = data.disabled_self_context || false
43
+ this.renderContexts()
44
+ }
45
+ } catch (e) {
46
+ console.error("Failed to load contexts", e)
47
+ }
48
+ }
49
+
50
+ toggleVisibility() {
51
+ this._hasBeenManuallyToggled = true
52
+ this.listVisible = !this.listVisible
53
+ this._updateListVisibility()
54
+ }
55
+
56
+ _updateListVisibility() {
57
+ if (!this.hasListTarget) return
58
+ this.listTarget.style.display = this.listVisible ? '' : 'none'
59
+
60
+ // Update toggle button active state
61
+ if (this.hasToggleButtonTarget) {
62
+ this.toggleButtonTarget.classList.toggle('context-toggle-active', this.listVisible)
63
+ }
64
+ }
65
+
66
+ _updateToggleButton() {
67
+ if (!this.hasToggleButtonTarget) return
68
+
69
+ const hasLinkedContexts = this.contexts.length > 0
70
+ // Always show button (self-context toggle is always available)
71
+ this.toggleButtonTarget.style.display = ''
72
+
73
+ // Show badge count when hidden
74
+ if (!this.listVisible) {
75
+ const activeLinked = this.contexts.filter(c => !c.disabled).length
76
+ const selfActive = this.selfContextDisabled ? 0 : 1
77
+ const total = activeLinked + selfActive
78
+ this.toggleButtonTarget.textContent = `🔗 ${total}`
79
+ } else {
80
+ this.toggleButtonTarget.textContent = '🔗'
81
+ }
82
+
83
+ // Auto-show if linked contexts exist, otherwise keep hidden
84
+ if (hasLinkedContexts && !this._hasBeenManuallyToggled) {
85
+ this.listVisible = true
86
+ this._updateListVisibility()
87
+ } else if (!hasLinkedContexts && !this.canManage && !this._hasBeenManuallyToggled) {
88
+ this.listVisible = false
89
+ this._updateListVisibility()
90
+ }
91
+ }
92
+
93
+ renderContexts() {
94
+ if (!this.hasListTarget) return
95
+
96
+ this._updateToggleButton()
97
+
98
+ const dragActions = this.canManage
99
+ ? 'dragstart->comments--contexts#handleDragStart dragend->comments--contexts#handleDragEnd'
100
+ : ''
101
+ const reorderActions = this.canManage
102
+ ? 'dragover->comments--contexts#handleReorderDragOver dragleave->comments--contexts#handleReorderDragLeave drop->comments--contexts#handleReorderDrop'
103
+ : ''
104
+
105
+ let html = ''
106
+
107
+ // Current creative self-context toggle (always first)
108
+ const selfDisabled = this.selfContextDisabled
109
+ const selfClass = selfDisabled ? 'context-disabled' : ''
110
+ const selfLabel = this._escapeHtml(this.currentCreativeSnippet || 'Self')
111
+ html += `<span class="context-chip context-self ${selfClass}"
112
+ data-action="click->comments--contexts#toggleSelfContext"
113
+ title="${this.selfContextLabel}">
114
+ 📌 ${selfLabel}
115
+ </span>`
116
+
117
+ this.contexts.forEach(ctx => {
118
+ const disabledClass = ctx.disabled ? 'context-disabled' : ''
119
+ const inheritedClass = ctx.inherited ? 'context-inherited' : ''
120
+ const draggable = this.canManage && !ctx.inherited ? 'draggable="true"' : ''
121
+
122
+ html += `<span class="context-chip ${disabledClass} ${inheritedClass}" ${draggable}
123
+ data-action="click->comments--contexts#toggleContext ${dragActions} ${reorderActions}"
124
+ data-context-id="${ctx.id}"
125
+ title="${ctx.inherited ? this.inheritedLabel : ''}">
126
+ 🔗 ${this._escapeHtml(ctx.description)}`
127
+
128
+ html += `<button class="navigate-context-btn" data-action="click->comments--contexts#navigateToContext" data-context-id="${ctx.id}" title="${this.navigateLabel}">\u2192</button>`
129
+
130
+ if (this.canManage && !ctx.inherited) {
131
+ html += `<button class="delete-context-btn" data-action="click->comments--contexts#removeContext" data-context-id="${ctx.id}">&times;</button>`
132
+ }
133
+
134
+ html += `</span>`
135
+ })
136
+
137
+ if (this.canManage) {
138
+ html += `<button class="add-context-btn" data-action="click->comments--contexts#addContext">+</button>`
139
+ }
140
+
141
+ this.listTarget.innerHTML = html
142
+ }
143
+
144
+ get inheritedLabel() {
145
+ return this.listTarget.dataset.inheritedLabel || 'Inherited from parent'
146
+ }
147
+
148
+ get selfContextLabel() {
149
+ return this.listTarget.dataset.selfContextLabel || 'Current creative context'
150
+ }
151
+
152
+ get navigateLabel() {
153
+ return this.listTarget.dataset.navigateLabel || 'Go to creative'
154
+ }
155
+
156
+ get currentCreativeSnippet() {
157
+ const popup = this.element.closest('#comments-popup')
158
+ return popup?.querySelector('#comments-popup-title')?.textContent?.trim()
159
+ }
160
+
161
+ get selfContextDisabled() {
162
+ return this._selfContextDisabled || false
163
+ }
164
+
165
+ toggleSelfContext(event) {
166
+ this._selfContextDisabled = !this._selfContextDisabled
167
+ this.renderContexts()
168
+ this._saveSelfContextState()
169
+ }
170
+
171
+ async _saveSelfContextState() {
172
+ await this._patchContexts({ disabled_self_context: this._selfContextDisabled })
173
+ }
174
+
175
+ navigateToContext(event) {
176
+ event.stopPropagation()
177
+ const contextId = event.currentTarget.dataset.contextId
178
+ if (!contextId) return
179
+ window.location.href = `/creatives/${contextId}`
180
+ }
181
+
182
+ toggleContext(event) {
183
+ if (event.target.closest('.delete-context-btn') || event.target.closest('.navigate-context-btn')) return
184
+
185
+ const contextId = parseInt(event.currentTarget.dataset.contextId)
186
+ if (!contextId) return
187
+
188
+ const ctx = this.contexts.find(c => c.id === contextId)
189
+ if (!ctx) return
190
+
191
+ ctx.disabled = !ctx.disabled
192
+ this.renderContexts()
193
+ this._saveDisabledState()
194
+ }
195
+
196
+ async removeContext(event) {
197
+ event.stopPropagation()
198
+ const contextId = parseInt(event.currentTarget.dataset.contextId)
199
+ if (!contextId) return
200
+
201
+ const ownContexts = this.contexts.filter(c => !c.inherited)
202
+ const newIds = ownContexts.filter(c => c.id !== contextId).map(c => c.id)
203
+
204
+ await this._updateContextIds(newIds)
205
+ await this.loadContexts()
206
+ }
207
+
208
+ addContext() {
209
+ // Use the existing link-creative modal
210
+ const linkController = window.Stimulus?.getControllerForElementAndIdentifier(
211
+ document.querySelector('[data-controller~="link-creative"]'),
212
+ 'link-creative'
213
+ )
214
+
215
+ if (!linkController) {
216
+ console.error("link-creative controller not found")
217
+ return
218
+ }
219
+
220
+ const addBtn = this.listTarget.querySelector('.add-context-btn')
221
+ const rect = addBtn?.getBoundingClientRect() || { top: 200, left: 200, bottom: 230, right: 230 }
222
+
223
+ linkController.open(rect, (selectedCreative) => {
224
+ this._addContextId(selectedCreative.id)
225
+ })
226
+ }
227
+
228
+ async _addContextId(creativeId) {
229
+ // Prevent adding self as context
230
+ const selfId = parseInt(this.creativeId)
231
+ if (creativeId === selfId) return
232
+
233
+ const ownContexts = this.contexts.filter(c => !c.inherited)
234
+ const existingIds = ownContexts.map(c => c.id)
235
+
236
+ if (existingIds.includes(creativeId)) return
237
+ // Also check inherited
238
+ if (this.contexts.some(c => c.id === creativeId)) return
239
+
240
+ const newIds = [...existingIds, creativeId]
241
+ await this._updateContextIds(newIds)
242
+ await this.loadContexts()
243
+ }
244
+
245
+ // --- Drag & Drop for reordering ---
246
+ handleDragStart(event) {
247
+ const chipEl = event.currentTarget
248
+ const contextId = chipEl.dataset.contextId
249
+ if (!contextId) { event.preventDefault(); return }
250
+
251
+ this.draggingContextId = contextId
252
+ event.dataTransfer.setData('application/x-context-id', contextId)
253
+ event.dataTransfer.effectAllowed = 'move'
254
+
255
+ requestAnimationFrame(() => {
256
+ chipEl.classList.add('context-dragging')
257
+ })
258
+ }
259
+
260
+ handleDragEnd(event) {
261
+ this.draggingContextId = null
262
+ event.currentTarget.classList.remove('context-dragging')
263
+ this.listTarget.querySelectorAll('.context-chip').forEach(el => {
264
+ el.classList.remove('context-drag-over-left', 'context-drag-over-right')
265
+ })
266
+ }
267
+
268
+ handleReorderDragOver(event) {
269
+ if (!event.dataTransfer.types.includes('application/x-context-id')) return
270
+ if (!this.draggingContextId) return
271
+
272
+ const targetEl = event.currentTarget
273
+ const targetId = targetEl.dataset.contextId
274
+ if (!targetId || targetId === this.draggingContextId) return
275
+ // Don't allow reorder onto inherited chips
276
+ const ctx = this.contexts.find(c => String(c.id) === targetId)
277
+ if (ctx?.inherited) return
278
+
279
+ event.preventDefault()
280
+ event.dataTransfer.dropEffect = 'move'
281
+
282
+ const rect = targetEl.getBoundingClientRect()
283
+ const midpoint = rect.left + rect.width / 2
284
+ const isLeft = event.clientX < midpoint
285
+
286
+ targetEl.classList.toggle('context-drag-over-left', isLeft)
287
+ targetEl.classList.toggle('context-drag-over-right', !isLeft)
288
+ }
289
+
290
+ handleReorderDragLeave(event) {
291
+ event.currentTarget.classList.remove('context-drag-over-left', 'context-drag-over-right')
292
+ }
293
+
294
+ async handleReorderDrop(event) {
295
+ event.preventDefault()
296
+
297
+ const targetEl = event.currentTarget
298
+ targetEl.classList.remove('context-drag-over-left', 'context-drag-over-right')
299
+
300
+ const draggedId = parseInt(event.dataTransfer.getData('application/x-context-id'))
301
+ const targetId = parseInt(targetEl.dataset.contextId)
302
+
303
+ if (!draggedId || !targetId || draggedId === targetId) return
304
+
305
+ const ownContexts = this.contexts.filter(c => !c.inherited)
306
+ const ids = ownContexts.map(c => c.id)
307
+
308
+ const draggedIndex = ids.indexOf(draggedId)
309
+ const targetIndex = ids.indexOf(targetId)
310
+ if (draggedIndex === -1 || targetIndex === -1) return
311
+
312
+ // Determine drop position
313
+ const rect = targetEl.getBoundingClientRect()
314
+ const midpoint = rect.left + rect.width / 2
315
+ const insertBefore = event.clientX < midpoint
316
+
317
+ ids.splice(draggedIndex, 1)
318
+ let newIndex = ids.indexOf(targetId)
319
+ if (!insertBefore) newIndex += 1
320
+ ids.splice(newIndex, 0, draggedId)
321
+
322
+ await this._updateContextIds(ids)
323
+ await this.loadContexts()
324
+ }
325
+
326
+ // --- API calls ---
327
+ async _updateContextIds(ids) {
328
+ await this._patchContexts({ context_ids: ids })
329
+ }
330
+
331
+ async _saveDisabledState() {
332
+ const disabledIds = this.contexts.filter(c => c.disabled).map(c => c.id)
333
+ await this._patchContexts({ disabled_context_ids: disabledIds })
334
+ }
335
+
336
+ async _patchContexts(params) {
337
+ const creativeId = this.creativeId
338
+ if (!creativeId) return
339
+
340
+ try {
341
+ const response = await fetch(`/creatives/${creativeId}/update_contexts`, {
342
+ method: 'PATCH',
343
+ headers: {
344
+ 'Content-Type': 'application/json',
345
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
346
+ },
347
+ body: JSON.stringify(params)
348
+ })
349
+
350
+ if (!response.ok) {
351
+ console.error('Failed to update contexts', params)
352
+ }
353
+ } catch (e) {
354
+ console.error('Error updating contexts', e)
355
+ }
356
+ }
357
+
358
+ _escapeHtml(text) {
359
+ const div = document.createElement('div')
360
+ div.textContent = text
361
+ return div.innerHTML
362
+ }
363
+ }