collavre 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/actiontext.css +73 -71
- data/app/assets/stylesheets/collavre/activity_logs.css +18 -45
- data/app/assets/stylesheets/collavre/comments_popup.css +197 -35
- data/app/assets/stylesheets/collavre/creatives.css +101 -51
- data/app/assets/stylesheets/collavre/dark_mode.css +221 -88
- data/app/assets/stylesheets/collavre/design_tokens.css +334 -0
- data/app/assets/stylesheets/collavre/mention_menu.css +13 -9
- data/app/assets/stylesheets/collavre/popup.css +57 -27
- data/app/assets/stylesheets/collavre/slide_view.css +6 -6
- data/app/assets/stylesheets/collavre/user_menu.css +4 -5
- data/app/components/collavre/plans_timeline_component.html.erb +2 -2
- data/app/controllers/collavre/admin/orchestration_controller.rb +9 -2
- data/app/controllers/collavre/admin/settings_controller.rb +199 -0
- data/app/controllers/collavre/comments/reactions_controller.rb +1 -9
- data/app/controllers/collavre/comments_controller.rb +39 -162
- data/app/controllers/collavre/creatives_controller.rb +18 -58
- data/app/controllers/collavre/users_controller.rb +31 -3
- data/app/helpers/collavre/application_helper.rb +97 -0
- data/app/helpers/collavre/creatives_helper.rb +10 -202
- data/app/javascript/collavre.js +0 -1
- data/app/javascript/components/creative_tree_row.js +3 -2
- data/app/javascript/controllers/comment_controller.js +309 -4
- data/app/javascript/controllers/comments/form_controller.js +52 -0
- data/app/javascript/controllers/comments/presence_controller.js +13 -0
- data/app/javascript/controllers/creatives/tree_controller.js +2 -1
- data/app/javascript/controllers/link_creative_controller.js +29 -3
- data/app/javascript/lib/__tests__/html_code_block_wrapper.test.js +201 -0
- data/app/javascript/lib/html_code_block_wrapper.js +168 -0
- data/app/javascript/lib/utils/markdown.js +2 -1
- data/app/javascript/modules/creative_row_editor.js +5 -1
- data/app/javascript/utils/emoji_parser.js +21 -0
- data/app/jobs/collavre/ai_agent_job.rb +6 -2
- data/app/jobs/collavre/cron_action_job.rb +18 -6
- data/app/jobs/collavre/cron_scheduler_job.rb +112 -0
- data/app/models/collavre/comment/approvable.rb +50 -0
- data/app/models/collavre/comment/broadcastable.rb +119 -0
- data/app/models/collavre/comment/notifiable.rb +111 -0
- data/app/models/collavre/comment.rb +13 -258
- data/app/models/collavre/comment_reaction.rb +15 -0
- data/app/models/collavre/creative/describable.rb +86 -0
- data/app/models/collavre/creative/linkable.rb +77 -0
- data/app/models/collavre/creative/permissible.rb +103 -0
- data/app/models/collavre/creative.rb +3 -289
- data/app/models/collavre/orchestrator_policy.rb +1 -1
- data/app/models/collavre/system_setting.rb +27 -1
- data/app/models/collavre/user.rb +42 -0
- data/app/models/collavre/user_theme.rb +10 -0
- data/app/services/collavre/ai_agent/approval_handler.rb +110 -0
- data/app/services/collavre/ai_agent/message_builder.rb +129 -0
- data/app/services/collavre/ai_agent/review_handler.rb +70 -0
- data/app/services/collavre/ai_agent_service.rb +93 -150
- data/app/services/collavre/ai_client.rb +23 -4
- data/app/services/collavre/auto_theme_generator.rb +168 -50
- data/app/services/collavre/command_menu_service.rb +70 -0
- data/app/services/collavre/comment_move_service.rb +94 -0
- data/app/services/collavre/comments/action_executor.rb +10 -0
- data/app/services/collavre/comments/mcp_command.rb +1 -2
- data/app/services/collavre/creatives/create_service.rb +86 -0
- data/app/services/collavre/creatives/destroy_service.rb +41 -0
- data/app/services/collavre/creatives/index_query.rb +3 -0
- data/app/services/collavre/markdown_converter.rb +240 -0
- data/app/services/collavre/mention_parser.rb +63 -0
- data/app/services/collavre/orchestration/agent_context_builder.rb +24 -8
- data/app/services/collavre/orchestration/agent_orchestrator.rb +59 -10
- data/app/services/collavre/orchestration/loop_breaker.rb +12 -7
- data/app/services/collavre/orchestration/policy_resolver.rb +16 -2
- data/app/services/collavre/orchestration/scheduler.rb +4 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
- data/app/services/collavre/system_events/context_builder.rb +1 -6
- data/app/services/collavre/tools/creative_batch_service.rb +107 -0
- data/app/services/collavre/tools/creative_update_service.rb +17 -12
- data/app/services/collavre/tools/cron_create_service.rb +17 -5
- data/app/views/admin/shared/_tabs.html.erb +2 -1
- data/app/views/collavre/admin/orchestration/show.html.erb +11 -0
- data/app/views/collavre/admin/settings/_system_tab.html.erb +138 -0
- data/app/views/collavre/admin/settings/_uiux_tab.html.erb +44 -0
- data/app/views/collavre/admin/settings/index.html.erb +11 -0
- data/app/views/collavre/admin/settings/uiux.html.erb +11 -0
- data/app/views/collavre/comments/_comment.html.erb +15 -5
- data/app/views/collavre/comments/_comments_popup.html.erb +9 -2
- data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +0 -3
- data/app/views/collavre/creatives/_share_button.html.erb +0 -52
- data/app/views/collavre/creatives/_share_modal.html.erb +52 -0
- data/app/views/collavre/creatives/index.html.erb +5 -8
- data/app/views/collavre/shared/navigation/_panels.html.erb +2 -2
- data/app/views/collavre/user_themes/index.html.erb +7 -9
- data/app/views/collavre/users/_contact_management.html.erb +2 -1
- data/app/views/collavre/users/edit_ai.html.erb +7 -0
- data/app/views/collavre/users/index.html.erb +16 -1
- data/app/views/collavre/users/new_ai.html.erb +18 -8
- data/app/views/collavre/users/passkeys.html.erb +1 -1
- data/app/views/collavre/users/show.html.erb +1 -1
- data/app/views/layouts/collavre/slide.html.erb +8 -1
- data/config/locales/admin.en.yml +88 -0
- data/config/locales/admin.ko.yml +88 -0
- data/config/locales/ai_agent.en.yml +5 -1
- data/config/locales/ai_agent.ko.yml +5 -1
- data/config/locales/comments.en.yml +5 -1
- data/config/locales/comments.ko.yml +5 -1
- data/config/locales/orchestration.en.yml +8 -0
- data/config/locales/orchestration.ko.yml +8 -0
- data/config/locales/users.en.yml +12 -0
- data/config/locales/users.ko.yml +12 -0
- data/config/routes.rb +7 -1
- data/db/migrate/20260212011655_add_quoted_comment_to_comments.rb +7 -0
- data/db/migrate/20260213044247_add_agent_conf_to_users.rb +5 -0
- data/lib/collavre/engine.rb +25 -0
- data/lib/collavre/version.rb +1 -1
- metadata +32 -1
|
@@ -1,30 +1,55 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class AutoThemeGenerator
|
|
3
3
|
REQUIRED_VARIABLES = %w[
|
|
4
|
-
--
|
|
5
|
-
--
|
|
4
|
+
--surface-bg
|
|
5
|
+
--surface-nav
|
|
6
|
+
--surface-section
|
|
7
|
+
--surface-input
|
|
8
|
+
--surface-btn
|
|
9
|
+
--surface-secondary
|
|
10
|
+
--text-primary
|
|
11
|
+
--text-muted
|
|
12
|
+
--text-on-btn
|
|
13
|
+
--text-nav
|
|
14
|
+
--text-nav-btn
|
|
15
|
+
--text-chat-btn
|
|
16
|
+
--text-on-badge
|
|
17
|
+
--text-input
|
|
6
18
|
--color-link
|
|
7
|
-
--color-
|
|
8
|
-
--color-
|
|
9
|
-
--color-
|
|
10
|
-
--color-
|
|
11
|
-
--color-
|
|
12
|
-
--color-
|
|
13
|
-
--color-complete
|
|
14
|
-
--color-chip-bg
|
|
15
|
-
--color-drag-over
|
|
16
|
-
--color-drag-over-edge
|
|
17
|
-
--hover-brightness
|
|
19
|
+
--color-brand
|
|
20
|
+
--color-active
|
|
21
|
+
--color-danger
|
|
22
|
+
--color-success
|
|
23
|
+
--color-warning
|
|
24
|
+
--color-highlight
|
|
18
25
|
--color-badge-bg
|
|
19
|
-
--color-
|
|
20
|
-
--color-
|
|
21
|
-
--color-
|
|
22
|
-
--color-
|
|
23
|
-
--color
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
26
|
+
--color-accent-border
|
|
27
|
+
--color-accent-text
|
|
28
|
+
--color-code-bg
|
|
29
|
+
--color-code-text
|
|
30
|
+
--border-color
|
|
31
|
+
--border-drag-over
|
|
32
|
+
--border-drag-edge
|
|
33
|
+
--hover-brightness
|
|
27
34
|
--creative-loading-emojis
|
|
35
|
+
--creative-h1-size
|
|
36
|
+
--creative-h2-size
|
|
37
|
+
--creative-h3-size
|
|
38
|
+
--creative-h1-weight
|
|
39
|
+
--creative-h2-weight
|
|
40
|
+
--creative-h3-weight
|
|
41
|
+
--creative-h1-color
|
|
42
|
+
--creative-h2-color
|
|
43
|
+
--creative-h3-color
|
|
44
|
+
--creative-childless-size
|
|
45
|
+
--creative-childless-weight
|
|
46
|
+
--creative-bullet-size
|
|
47
|
+
--creative-bullet-color
|
|
48
|
+
--creative-tree-line-color
|
|
49
|
+
--creative-tree-line-opacity
|
|
50
|
+
--creative-h1-bg
|
|
51
|
+
--creative-h2-bg
|
|
52
|
+
--creative-h3-bg
|
|
28
53
|
].freeze
|
|
29
54
|
|
|
30
55
|
def initialize(client: default_client)
|
|
@@ -33,35 +58,128 @@ module Collavre
|
|
|
33
58
|
|
|
34
59
|
def generate(prompt)
|
|
35
60
|
system_prompt = <<~PROMPT
|
|
36
|
-
You are
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
You are a color theme designer for a workspace app.
|
|
62
|
+
Generate a JSON with ONLY these keys: #{REQUIRED_VARIABLES.join(', ')}.
|
|
63
|
+
|
|
64
|
+
FORMAT: hex colors (#rrggbb). --hover-brightness: "90%" (light) or "110%" (dark).
|
|
65
|
+
--creative-loading-emojis: 6 emojis. Return ONLY valid JSON, no markdown.
|
|
66
|
+
|
|
67
|
+
=== REFERENCE THEMES (these are GOOD — match this quality) ===
|
|
68
|
+
|
|
69
|
+
"바나나" (yellow, light):
|
|
70
|
+
{"--surface-bg":"#fdf7d0","--surface-nav":"#e8d36b","--surface-section":"#feeec1",
|
|
71
|
+
"--surface-input":"#ffffff","--surface-btn":"#e3b831","--surface-secondary":"#f4d9bb",
|
|
72
|
+
"--text-primary":"#1a1200","--text-muted":"#5a4e00","--text-on-btn":"#241100",
|
|
73
|
+
"--text-nav":"#1a1200","--text-nav-btn":"#1a1200","--text-chat-btn":"#1a1200",
|
|
74
|
+
"--text-on-badge":"#fff8e0","--text-input":"#1a1200",
|
|
75
|
+
"--color-link":"#c69900","--color-brand":"#c69900","--color-active":"#c08500",
|
|
76
|
+
"--color-danger":"#cc3300","--color-success":"#4a8c00","--color-warning":"#cc8800",
|
|
77
|
+
"--color-highlight":"#fff3a0","--color-badge-bg":"#ca9600",
|
|
78
|
+
"--color-accent-border":"#a67a00","--color-accent-text":"#c69900",
|
|
79
|
+
"--color-code-bg":"#f5efc0","--color-code-text":"#1a1200",
|
|
80
|
+
"--border-color":"#c4b060","--border-drag-over":"#d4a800","--border-drag-edge":"#c69900",
|
|
81
|
+
"--hover-brightness":"90%","--creative-loading-emojis":"🍌🌻💛🍋✨🌼",
|
|
82
|
+
"--creative-h1-size":"1.3em","--creative-h2-size":"1.2em","--creative-h3-size":"1.1em",
|
|
83
|
+
"--creative-h1-weight":"700","--creative-h2-weight":"600","--creative-h3-weight":"500",
|
|
84
|
+
"--creative-h1-color":"#1a1200","--creative-h2-color":"#1a1200","--creative-h3-color":"#5a4e00",
|
|
85
|
+
"--creative-childless-size":"1em","--creative-childless-weight":"400",
|
|
86
|
+
"--creative-bullet-size":"5px","--creative-bullet-color":"#1a1200",
|
|
87
|
+
"--creative-tree-line-color":"#c4b060","--creative-tree-line-opacity":"0.5",
|
|
88
|
+
"--creative-h1-bg":"transparent","--creative-h2-bg":"transparent","--creative-h3-bg":"transparent"}
|
|
89
|
+
|
|
90
|
+
"숲속" (green, light):
|
|
91
|
+
{"--surface-bg":"#ecf4ef","--surface-nav":"#d5e2d7","--surface-section":"#e2f4e7",
|
|
92
|
+
"--surface-input":"#f6f9f7","--surface-btn":"#357153","--surface-secondary":"#dbe8e3",
|
|
93
|
+
"--text-primary":"#0a1f10","--text-muted":"#3a5040","--text-on-btn":"#f3fbf6",
|
|
94
|
+
"--text-nav":"#0a1f10","--text-nav-btn":"#0a1f10","--text-chat-btn":"#0a1f10",
|
|
95
|
+
"--text-on-badge":"#f3fbf6","--text-input":"#0a1f10",
|
|
96
|
+
"--color-link":"#25984d","--color-brand":"#25984d","--color-active":"#008a39",
|
|
97
|
+
"--color-danger":"#cc3300","--color-success":"#00880a","--color-warning":"#cc8800",
|
|
98
|
+
"--color-highlight":"#c8f0d0","--color-badge-bg":"#005734",
|
|
99
|
+
"--color-accent-border":"#337344","--color-accent-text":"#1a3520",
|
|
100
|
+
"--color-code-bg":"#e0ede4","--color-code-text":"#0a1f10",
|
|
101
|
+
"--border-color":"#a0bca6","--border-drag-over":"#40905a","--border-drag-edge":"#25984d",
|
|
102
|
+
"--hover-brightness":"90%","--creative-loading-emojis":"🌲🍃🌿🌱✨🦎",
|
|
103
|
+
"--creative-h1-size":"1.3em","--creative-h2-size":"1.15em","--creative-h3-size":"1.05em",
|
|
104
|
+
"--creative-h1-weight":"700","--creative-h2-weight":"600","--creative-h3-weight":"500",
|
|
105
|
+
"--creative-h1-color":"#0a1f10","--creative-h2-color":"#0a1f10","--creative-h3-color":"#3a5040",
|
|
106
|
+
"--creative-childless-size":"1em","--creative-childless-weight":"400",
|
|
107
|
+
"--creative-bullet-size":"5px","--creative-bullet-color":"#25984d",
|
|
108
|
+
"--creative-tree-line-color":"#a0bca6","--creative-tree-line-opacity":"0.5",
|
|
109
|
+
"--creative-h1-bg":"transparent","--creative-h2-bg":"transparent","--creative-h3-bg":"transparent"}
|
|
110
|
+
|
|
111
|
+
"토마토" (red/warm, light):
|
|
112
|
+
{"--surface-bg":"#fbefea","--surface-nav":"#f7ded6","--surface-section":"#f5e8e4",
|
|
113
|
+
"--surface-input":"#fff6f3","--surface-btn":"#cc0000","--surface-secondary":"#f0cfc4",
|
|
114
|
+
"--text-primary":"#1f0800","--text-muted":"#5a3020","--text-on-btn":"#fff0e8",
|
|
115
|
+
"--text-nav":"#1f0800","--text-nav-btn":"#1f0800","--text-chat-btn":"#1f0800",
|
|
116
|
+
"--text-on-badge":"#fff0e8","--text-input":"#1f0800",
|
|
117
|
+
"--color-link":"#d40924","--color-brand":"#d40924","--color-active":"#ff4040",
|
|
118
|
+
"--color-danger":"#cc0000","--color-success":"#4a8c00","--color-warning":"#cc8800",
|
|
119
|
+
"--color-highlight":"#ffe0d0","--color-badge-bg":"#ba0d01",
|
|
120
|
+
"--color-accent-border":"#c85b32","--color-accent-text":"#b22800",
|
|
121
|
+
"--color-code-bg":"#f0e4de","--color-code-text":"#1f0800",
|
|
122
|
+
"--border-color":"#d0a898","--border-drag-over":"#e04020","--border-drag-edge":"#cc0000",
|
|
123
|
+
"--hover-brightness":"90%","--creative-loading-emojis":"🍅🔴🌶️🫕✨🍝",
|
|
124
|
+
"--creative-h1-size":"1.3em","--creative-h2-size":"1.2em","--creative-h3-size":"1.1em",
|
|
125
|
+
"--creative-h1-weight":"700","--creative-h2-weight":"600","--creative-h3-weight":"500",
|
|
126
|
+
"--creative-h1-color":"#1f0800","--creative-h2-color":"#1f0800","--creative-h3-color":"#5a3020",
|
|
127
|
+
"--creative-childless-size":"1em","--creative-childless-weight":"400",
|
|
128
|
+
"--creative-bullet-size":"5px","--creative-bullet-color":"#d40924",
|
|
129
|
+
"--creative-tree-line-color":"#d0a898","--creative-tree-line-opacity":"0.5",
|
|
130
|
+
"--creative-h1-bg":"transparent","--creative-h2-bg":"transparent","--creative-h3-bg":"transparent"}
|
|
131
|
+
|
|
132
|
+
"사이버펑크" (neon, dark):
|
|
133
|
+
{"--surface-bg":"#070b14","--surface-nav":"#0f101f","--surface-section":"#12161f",
|
|
134
|
+
"--surface-input":"#02060d","--surface-btn":"#2f1d4a","--surface-secondary":"#181b1f",
|
|
135
|
+
"--text-primary":"#dcdde5","--text-muted":"#8a8c99","--text-on-btn":"#e0d0ff",
|
|
136
|
+
"--text-nav":"#dcdde5","--text-nav-btn":"#dcdde5","--text-chat-btn":"#dcdde5",
|
|
137
|
+
"--text-on-badge":"#dcdde5","--text-input":"#dcdde5",
|
|
138
|
+
"--color-link":"#0099f0","--color-brand":"#e749df","--color-active":"#0089e9",
|
|
139
|
+
"--color-danger":"#ff3050","--color-success":"#00cc66","--color-warning":"#ffaa00",
|
|
140
|
+
"--color-highlight":"#2a1848","--color-badge-bg":"#692278",
|
|
141
|
+
"--color-accent-border":"#0094c9","--color-accent-text":"#eb63c5",
|
|
142
|
+
"--color-code-bg":"#0e1220","--color-code-text":"#c0c4d0",
|
|
143
|
+
"--border-color":"#2a2e3a","--border-drag-over":"#6030a0","--border-drag-edge":"#e749df",
|
|
144
|
+
"--hover-brightness":"110%","--creative-loading-emojis":"🌃💜⚡🤖✨🎮",
|
|
145
|
+
"--creative-h1-size":"1.4em","--creative-h2-size":"1.2em","--creative-h3-size":"1.1em",
|
|
146
|
+
"--creative-h1-weight":"800","--creative-h2-weight":"600","--creative-h3-weight":"500",
|
|
147
|
+
"--creative-h1-color":"#e749df","--creative-h2-color":"#0099f0","--creative-h3-color":"#8a8c99",
|
|
148
|
+
"--creative-childless-size":"1em","--creative-childless-weight":"400",
|
|
149
|
+
"--creative-bullet-size":"4px","--creative-bullet-color":"#e749df",
|
|
150
|
+
"--creative-tree-line-color":"#2a2e3a","--creative-tree-line-opacity":"0.7",
|
|
151
|
+
"--creative-h1-bg":"#0f101f","--creative-h2-bg":"#12161f","--creative-h3-bg":"transparent"}
|
|
152
|
+
|
|
153
|
+
=== RULES ===
|
|
154
|
+
|
|
155
|
+
1) SURFACES must be TINTED with the theme color — never gray or neutral.
|
|
156
|
+
bg is lightest (pastel), then input, section, secondary, nav (deeper), btn (deepest or vivid).
|
|
157
|
+
2) ALL text must be DARK (#0a-#2f range) on light themes, LIGHT (#cc-#ff range) on dark themes.
|
|
158
|
+
Exception: text-on-btn must contrast with surface-btn. If btn is dark, text-on-btn is light.
|
|
159
|
+
3) brand/link = vivid theme color. danger=red, success=green, warning=amber ALWAYS.
|
|
160
|
+
4) code-bg = same hue as surface-bg, slightly darker. NEVER a different hue family.
|
|
161
|
+
5) Multi-color prompts (e.g. "Google logo", "rainbow", "Italian flag"):
|
|
162
|
+
- Surfaces: use the DOMINANT color's hue (tinted, not gray)
|
|
163
|
+
- Distribute each distinct color to: brand, active, badge-bg, accent-border, accent-text
|
|
164
|
+
- Each color must be RECOGNIZABLE — never blend into one muddy tone
|
|
165
|
+
6) Match the vibe: "토마토" = warm reds/oranges, "바나나" = bright yellows, "숲" = rich greens.
|
|
166
|
+
The user should IMMEDIATELY recognize the theme from its colors.
|
|
167
|
+
7) CREATIVE TREE STYLING — the app has a hierarchical list (outliner) with levels:
|
|
168
|
+
- Level 1-3: headings (h1/h2/h3). Level 4+: bullet items.
|
|
169
|
+
- Items at level 1-3 WITHOUT children render as plain text (childless style).
|
|
170
|
+
- creative-h{1,2,3}-size: font size (em units). h1 largest, descending. Range: 1.0em-1.5em.
|
|
171
|
+
- creative-h{1,2,3}-weight: font weight. h1 boldest. Values: 400-800.
|
|
172
|
+
- creative-h{1,2,3}-color: heading colors. Use text-primary or theme accent for h1.
|
|
173
|
+
h3 can use muted color. MUST be readable on surface-bg.
|
|
174
|
+
- creative-childless-size: usually 1em (normal text size).
|
|
175
|
+
- creative-childless-weight: usually 400 (normal weight).
|
|
176
|
+
- creative-bullet-size: bullet dot diameter in px. Range: 3px-6px.
|
|
177
|
+
- creative-bullet-color: bullet color. Can match brand or text-primary.
|
|
178
|
+
- creative-tree-line-color: vertical guide line color. Usually border-color or muted.
|
|
179
|
+
- creative-tree-line-opacity: 0.3-0.8. Subtle but visible.
|
|
180
|
+
- creative-h{1,2,3}-bg: heading row background color. Usually "transparent".
|
|
181
|
+
For dark/accent themes, h1 can use a subtle darker surface. Must not clash with text.
|
|
182
|
+
- For playful themes, vary heading colors. For minimal themes, keep sizes uniform.
|
|
65
183
|
PROMPT
|
|
66
184
|
|
|
67
185
|
response = @client.chat([
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class CommandMenuService
|
|
3
|
+
def initialize(user:)
|
|
4
|
+
@user = user
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def items
|
|
8
|
+
built_in_items + mcp_items
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
attr_reader :user
|
|
14
|
+
|
|
15
|
+
def built_in_items
|
|
16
|
+
[
|
|
17
|
+
{
|
|
18
|
+
name: "calendar",
|
|
19
|
+
label: "/calendar",
|
|
20
|
+
aliases: [ "/cal" ],
|
|
21
|
+
description: I18n.t("collavre.comments.command_menu.calendar_description"),
|
|
22
|
+
args: I18n.t("collavre.comments.command_menu.calendar_args")
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "topic",
|
|
26
|
+
label: "/topic",
|
|
27
|
+
description: I18n.t("collavre.comments.command_menu.topic_description"),
|
|
28
|
+
args: I18n.t("collavre.comments.command_menu.topic_args")
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mcp_items
|
|
34
|
+
Collavre::McpService.available_tools(user).filter_map do |tool|
|
|
35
|
+
tool_name = tool[:name] || tool["name"]
|
|
36
|
+
next unless tool_name
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
name: tool_name,
|
|
40
|
+
label: "/#{tool_name}",
|
|
41
|
+
description: tool[:description] || tool["description"],
|
|
42
|
+
args: format_args(tool[:params] || tool["params"])
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def format_args(params)
|
|
48
|
+
return if params.blank?
|
|
49
|
+
|
|
50
|
+
if params.is_a?(Array)
|
|
51
|
+
return params.map do |param|
|
|
52
|
+
name = param[:name] || param["name"]
|
|
53
|
+
required = param[:required] || param["required"]
|
|
54
|
+
name.to_s + (required ? "*" : "")
|
|
55
|
+
end.join(", ")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
properties = params[:properties] || params["properties"]
|
|
59
|
+
return unless properties.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
required = params[:required] || params["required"] || []
|
|
62
|
+
required = Array(required).map(&:to_s)
|
|
63
|
+
|
|
64
|
+
properties.keys.map do |key|
|
|
65
|
+
key = key.to_s
|
|
66
|
+
required.include?(key) ? "#{key}*" : key
|
|
67
|
+
end.join(", ")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class CommentMoveService
|
|
3
|
+
class MoveError < StandardError; end
|
|
4
|
+
|
|
5
|
+
def initialize(creative:, user:)
|
|
6
|
+
@creative = creative
|
|
7
|
+
@user = user
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns { success: true, moved_count: N }
|
|
11
|
+
# Raises MoveError on validation failures
|
|
12
|
+
def call(comment_ids:, target_creative_id: nil, target_topic_id: nil)
|
|
13
|
+
comment_ids = Array(comment_ids).map(&:presence).compact.map(&:to_i)
|
|
14
|
+
raise MoveError, I18n.t("collavre.comments.move_no_selection") if comment_ids.empty?
|
|
15
|
+
|
|
16
|
+
target_origin, new_topic_id = resolve_target(target_creative_id, target_topic_id)
|
|
17
|
+
|
|
18
|
+
validate_permissions!(target_origin)
|
|
19
|
+
comments = fetch_visible_comments(comment_ids)
|
|
20
|
+
|
|
21
|
+
moved_count = perform_move(comments, target_origin, new_topic_id)
|
|
22
|
+
|
|
23
|
+
Comment.broadcast_badges(@creative)
|
|
24
|
+
Comment.broadcast_badges(target_origin) unless target_origin == @creative
|
|
25
|
+
|
|
26
|
+
{ success: true, moved_count: moved_count }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :creative, :user
|
|
32
|
+
|
|
33
|
+
def resolve_target(target_creative_id, target_topic_id)
|
|
34
|
+
if target_creative_id.present?
|
|
35
|
+
target_creative = Creative.find_by(id: target_creative_id)
|
|
36
|
+
raise MoveError, I18n.t("collavre.comments.move_invalid_target") unless target_creative
|
|
37
|
+
[ target_creative.effective_origin, nil ]
|
|
38
|
+
elsif !target_topic_id.nil?
|
|
39
|
+
new_topic_id = target_topic_id.presence
|
|
40
|
+
if new_topic_id.present? && !creative.topics.exists?(id: new_topic_id)
|
|
41
|
+
raise MoveError, I18n.t("collavre.comments.move_invalid_topic", default: "Invalid topic")
|
|
42
|
+
end
|
|
43
|
+
[ creative, new_topic_id ]
|
|
44
|
+
else
|
|
45
|
+
raise MoveError, I18n.t("collavre.comments.move_invalid_target")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_permissions!(target_origin)
|
|
50
|
+
unless creative.has_permission?(user, :feedback) && target_origin.has_permission?(user, :feedback)
|
|
51
|
+
raise MoveError, I18n.t("collavre.comments.move_not_allowed")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fetch_visible_comments(comment_ids)
|
|
56
|
+
scope = creative.comments.where(
|
|
57
|
+
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
58
|
+
false, user.id, user.id
|
|
59
|
+
)
|
|
60
|
+
comments = scope.where(id: comment_ids).to_a
|
|
61
|
+
raise MoveError, I18n.t("collavre.comments.move_not_allowed") if comments.length != comment_ids.length
|
|
62
|
+
comments
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def perform_move(comments, target_origin, new_topic_id)
|
|
66
|
+
moved_count = 0
|
|
67
|
+
ActiveRecord::Base.transaction do
|
|
68
|
+
comments.each do |comment|
|
|
69
|
+
same_creative = comment.creative_id == target_origin.id
|
|
70
|
+
same_topic = comment.topic_id.to_s == new_topic_id.to_s
|
|
71
|
+
next if same_creative && same_topic
|
|
72
|
+
|
|
73
|
+
if same_creative
|
|
74
|
+
comment.update!(topic_id: new_topic_id)
|
|
75
|
+
else
|
|
76
|
+
comment.update!(creative: target_origin, topic_id: new_topic_id)
|
|
77
|
+
broadcast_move_removal(comment, comment.creative)
|
|
78
|
+
end
|
|
79
|
+
moved_count += 1
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
moved_count
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def broadcast_move_removal(comment, original_creative)
|
|
86
|
+
return if comment.private?
|
|
87
|
+
|
|
88
|
+
Turbo::StreamsChannel.broadcast_remove_to(
|
|
89
|
+
[ original_creative, :comments ],
|
|
90
|
+
target: ActionView::RecordIdentifier.dom_id(comment)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -73,6 +73,7 @@ module Collavre
|
|
|
73
73
|
SUPPORTED_ACTIONS = {
|
|
74
74
|
"create_creative" => :create_creative,
|
|
75
75
|
"update_creative" => :update_creative,
|
|
76
|
+
"delete_creative" => :delete_creative,
|
|
76
77
|
"approve_tool" => :approve_tool,
|
|
77
78
|
"execute_tool" => :execute_tool
|
|
78
79
|
}.freeze
|
|
@@ -150,6 +151,15 @@ module Collavre
|
|
|
150
151
|
raise InvalidActionError, e.record.errors.full_messages.to_sentence
|
|
151
152
|
end
|
|
152
153
|
|
|
154
|
+
def delete_creative(payload)
|
|
155
|
+
creative = find_target_creative(payload)
|
|
156
|
+
executor = comment.user || Current.user
|
|
157
|
+
unless creative.has_permission?(executor, :write)
|
|
158
|
+
raise InvalidActionError, I18n.t("collavre.comments.approve_no_write_permission")
|
|
159
|
+
end
|
|
160
|
+
creative.destroy!
|
|
161
|
+
end
|
|
162
|
+
|
|
153
163
|
def approve_tool(payload)
|
|
154
164
|
tool_name = payload["tool_name"]
|
|
155
165
|
raise InvalidActionError, "Tool name is required" if tool_name.blank?
|
|
@@ -79,8 +79,7 @@ module Collavre
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def format_response(response)
|
|
82
|
-
|
|
83
|
-
return "Error running /#{tool_name}: #{response[:error]}" if response[:error].present?
|
|
82
|
+
return I18n.t("collavre.comments.mcp_command.error_running", tool_name: tool_name, error: response[:error]) if response[:error].present?
|
|
84
83
|
|
|
85
84
|
result = response[:result]
|
|
86
85
|
content = case result
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Creatives
|
|
5
|
+
# Handles creative creation including sequencing and tagging.
|
|
6
|
+
class CreateService
|
|
7
|
+
Result = Struct.new(:creative, :success?, :errors, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
def initialize(creative_params:, user:, child_id: nil, before_id: nil, after_id: nil, tag_ids: nil)
|
|
10
|
+
@creative_params = creative_params
|
|
11
|
+
@user = user
|
|
12
|
+
@child_id = child_id
|
|
13
|
+
@before_id = before_id
|
|
14
|
+
@after_id = after_id
|
|
15
|
+
@tag_ids = Array(tag_ids).compact
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
creative = Creative.new(@creative_params)
|
|
20
|
+
creative.user = creative.parent ? creative.parent.user : @user
|
|
21
|
+
|
|
22
|
+
unless creative.save
|
|
23
|
+
return Result.new(creative: creative, success?: false, errors: creative.errors.full_messages)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
reparent_child(creative)
|
|
27
|
+
insert_at_position(creative)
|
|
28
|
+
apply_tags(creative)
|
|
29
|
+
|
|
30
|
+
Result.new(creative: creative, success?: true, errors: [])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def reparent_child(creative)
|
|
36
|
+
return unless @child_id.present?
|
|
37
|
+
|
|
38
|
+
child = Creative.find_by(id: @child_id)
|
|
39
|
+
child&.update(parent: creative)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def insert_at_position(creative)
|
|
43
|
+
if @before_id.present?
|
|
44
|
+
insert_before(creative, @before_id)
|
|
45
|
+
elsif @after_id.present?
|
|
46
|
+
insert_after(creative, @after_id)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def insert_before(creative, before_id)
|
|
51
|
+
before_creative = Creative.find_by(id: before_id)
|
|
52
|
+
return unless before_creative && before_creative.parent_id == creative.parent_id
|
|
53
|
+
|
|
54
|
+
siblings = fetch_siblings(creative)
|
|
55
|
+
index = siblings.index { |s| s.id == before_creative.id } || 0
|
|
56
|
+
siblings.insert(index, creative)
|
|
57
|
+
resequence(siblings)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def insert_after(creative, after_id)
|
|
61
|
+
after_creative = Creative.find_by(id: after_id)
|
|
62
|
+
return unless after_creative && after_creative.parent_id == creative.parent_id
|
|
63
|
+
|
|
64
|
+
siblings = fetch_siblings(creative)
|
|
65
|
+
index = siblings.index { |s| s.id == after_creative.id } || -1
|
|
66
|
+
siblings.insert(index + 1, creative)
|
|
67
|
+
resequence(siblings)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fetch_siblings(creative)
|
|
71
|
+
collection = creative.parent ? creative.parent.children.order(:sequence) : Creative.roots.order(:sequence)
|
|
72
|
+
collection.to_a.reject { |s| s.id == creative.id }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def resequence(siblings)
|
|
76
|
+
siblings.each_with_index { |c, idx| c.update_column(:sequence, idx) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def apply_tags(creative)
|
|
80
|
+
@tag_ids.each do |tag_id|
|
|
81
|
+
creative.tags.create(label_id: tag_id)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Creatives
|
|
5
|
+
# Handles creative destruction with optional recursive child deletion.
|
|
6
|
+
class DestroyService
|
|
7
|
+
def initialize(creative:, user:, delete_with_children: false)
|
|
8
|
+
@creative = creative
|
|
9
|
+
@user = user
|
|
10
|
+
@delete_with_children = delete_with_children
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
if @delete_with_children
|
|
15
|
+
destroy_descendants_recursively(@creative)
|
|
16
|
+
else
|
|
17
|
+
reparent_children
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
CreativeShare.where(creative: @creative).destroy_all
|
|
21
|
+
@creative.destroy
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def reparent_children
|
|
27
|
+
parent = @creative.parent
|
|
28
|
+
@creative.children.each { |child| child.update(parent: parent) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def destroy_descendants_recursively(creative)
|
|
32
|
+
deletable_children = creative.children_with_permission(@user, :admin)
|
|
33
|
+
deletable_children.each do |child|
|
|
34
|
+
destroy_descendants_recursively(child)
|
|
35
|
+
CreativeShare.where(creative: child).destroy_all
|
|
36
|
+
child.destroy
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -82,6 +82,9 @@ module Creatives
|
|
|
82
82
|
# Sort by comment updated_at for comment filter
|
|
83
83
|
if params[:comment] == "true"
|
|
84
84
|
matched_creatives = matched_creatives.sort_by { |c| c.comments.maximum(:updated_at) || c.updated_at }.reverse
|
|
85
|
+
elsif params[:search].present? && params[:simple].present?
|
|
86
|
+
# Sort by description length (shorter = more relevant match)
|
|
87
|
+
matched_creatives = matched_creatives.sort_by { |c| c.description.to_s.length }
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
parent = params[:id] ? Creative.find_by(id: params[:id]) : nil
|