Class: Collavre::User
- Inherits:
-
ApplicationRecord
- Object
- ApplicationRecord
- ApplicationRecord
- Collavre::User
- Includes:
- HasInboxCreative
- Defined in:
- app/models/collavre/user.rb
Constant Summary collapse
- AGENT_CONF_DEFAULTS =
Default context settings for AI agents
{ "context" => { "chat_history" => 50, "chat_history_size" => 100_000, "creative_children_level" => nil }, "session" => { "enabled" => nil } }.freeze
- DEFAULT_CREATIVE_CHILDREN_LEVEL =
Default creative children level when agent_conf doesn't specify one
6- TYPO_CORRECTION_DEVICES =
%w[voice soft_keyboard physical_keyboard].freeze
- TYPO_CORRECTION_LOCATIONS =
%w[chat editor].freeze
- SUPPORTED_LLM_MODELS =
[ "gemini-3.1-flash-lite", "gemini-1.5-flash", "gemini-1.5-pro" ].freeze
Class Method Summary collapse
- .accessible_ai_agents_for(user) ⇒ Object
-
.const_missing(name) ⇒ Object
LLM_VENDOR_OPTIONS is resolved dynamically from the AiClient vendor-option registry so core lists only its built-in providers while vendor engines (e.g. OpenClaw) contribute their own.
- .mentionable_for(creative) ⇒ Object
Instance Method Summary collapse
- #ai_user? ⇒ Boolean
-
#chat_history_limit ⇒ Object
Convenience accessors for context settings.
- #chat_history_size_limit ⇒ Object
- #claude_channel_agent? ⇒ Boolean
-
#creative_children_level ⇒ Object
Returns the creative children depth level for AI context.
-
#destroy_creatives_leaf_first ⇒ Object
Destroy creatives deepest-first so closure_tree always finds its parent.
- #display_name ⇒ Object
- #email_verified? ⇒ Boolean
- #lock_account! ⇒ Object
-
#locked? ⇒ Boolean
Account lockout methods.
-
#parsed_agent_conf ⇒ Object
Returns parsed agent_conf merged with defaults.
- #password_meets_minimum_length ⇒ Object
-
#preserve_durable_summary_comments ⇒ Object
Keep durable compress/merge summaries (snapshot result comments) alive when their author is deleted: nullify authorship instead of cascading destroy.
- #record_failed_login! ⇒ Object
- #remaining_lockout_time ⇒ Object
- #reset_failed_login_attempts! ⇒ Object
-
#supports_session? ⇒ Boolean
Whether this agent uses stateful sessions (incremental messaging).
- #theme_accessibility ⇒ Object
-
#typo_correction_active_for?(device:, location:) ⇒ Boolean
2D gating: typo correction runs only when the master switch is on AND the originating typing device AND the input location are both enabled.
- #unlock_account! ⇒ Object
Methods included from HasInboxCreative
Class Method Details
.accessible_ai_agents_for(user) ⇒ Object
216 217 218 219 220 |
# File 'app/models/collavre/user.rb', line 216 def self.accessible_ai_agents_for(user) owned = ai_agents.where(created_by_id: user.id) searchable = ai_agents.where(searchable: true) owned.or(searchable).distinct.order(:name) end |
.const_missing(name) ⇒ Object
LLM_VENDOR_OPTIONS is resolved dynamically from the AiClient vendor-option registry so core lists only its built-in providers while vendor engines (e.g. OpenClaw) contribute their own. Resolved lazily via const_missing so the value always reflects registrations made after this class first loads (they run in a to_prepare hook), and existing views keep referring to the constant unchanged.
194 195 196 197 198 |
# File 'app/models/collavre/user.rb', line 194 def self.const_missing(name) return Collavre::AiClient. if name == :LLM_VENDOR_OPTIONS super end |
.mentionable_for(creative) ⇒ Object
222 223 224 225 226 227 228 229 230 |
# File 'app/models/collavre/user.rb', line 222 def self.mentionable_for(creative) scope = where(searchable: true) return scope unless creative origin = creative.effective_origin permitted_users = [ origin.user ].compact + origin.all_shared_users(:feedback).map(&:user) permitted_ids = permitted_users.compact.map(&:id) permitted_ids.any? ? scope.or(where(id: permitted_ids)) : scope end |
Instance Method Details
#ai_user? ⇒ Boolean
206 207 208 |
# File 'app/models/collavre/user.rb', line 206 def ai_user? llm_vendor.present? end |
#chat_history_limit ⇒ Object
Convenience accessors for context settings
118 119 120 |
# File 'app/models/collavre/user.rb', line 118 def chat_history_limit parsed_agent_conf.dig("context", "chat_history") || 50 end |
#chat_history_size_limit ⇒ Object
122 123 124 |
# File 'app/models/collavre/user.rb', line 122 def chat_history_size_limit parsed_agent_conf.dig("context", "chat_history_size") || 100_000 end |
#claude_channel_agent? ⇒ Boolean
210 211 212 |
# File 'app/models/collavre/user.rb', line 210 def claude_channel_agent? llm_model == "claude-code" end |
#creative_children_level ⇒ Object
Returns the creative children depth level for AI context. nil in agent_conf means use the default (6). 0 = no children, 1 = direct children only, 2 = grandchildren, etc.
129 130 131 132 |
# File 'app/models/collavre/user.rb', line 129 def creative_children_level level = parsed_agent_conf.dig("context", "creative_children_level") level.nil? ? DEFAULT_CREATIVE_CHILDREN_LEVEL : level.to_i end |
#destroy_creatives_leaf_first ⇒ Object
Destroy creatives deepest-first so closure_tree always finds its parent
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'app/models/collavre/user.rb', line 322 def destroy_creatives_leaf_first all_creatives = creatives.flat_map { |c| c.self_and_descendants.to_a }.uniq # Order by depth in memory. The subtree is contiguous within all_creatives # (every node between an owned root and its descendant is itself a # descendant), so following parent_id links yields the same leaf-first # ordering as self_and_ancestors.count without firing a COUNT query per # creative (the previous N+1). by_id = all_creatives.index_by(&:id) depth_of = lambda do |creative| depth = 0 node = creative while node&.parent_id && (parent = by_id[node.parent_id]) depth += 1 node = parent end depth end all_creatives.sort_by { |c| -depth_of.call(c) }.each do |c| c.reload.destroy! if Creative.exists?(c.id) end end |
#display_name ⇒ Object
261 262 263 |
# File 'app/models/collavre/user.rb', line 261 def display_name name.presence || email end |
#email_verified? ⇒ Boolean
257 258 259 |
# File 'app/models/collavre/user.rb', line 257 def email_verified? email_verified_at.present? end |
#lock_account! ⇒ Object
270 271 272 |
# File 'app/models/collavre/user.rb', line 270 def lock_account! update_columns(locked_at: Time.current) end |
#locked? ⇒ Boolean
Account lockout methods
266 267 268 |
# File 'app/models/collavre/user.rb', line 266 def locked? locked_at.present? && locked_at > Collavre::SystemSetting.lockout_duration.ago end |
#parsed_agent_conf ⇒ Object
Returns parsed agent_conf merged with defaults
106 107 108 109 110 111 112 113 114 115 |
# File 'app/models/collavre/user.rb', line 106 def parsed_agent_conf defaults = AGENT_CONF_DEFAULTS.deep_dup return defaults if agent_conf.blank? user_conf = YAML.safe_load(agent_conf, permitted_classes: [ Symbol ]) || {} defaults.deep_merge(user_conf) rescue Psych::SyntaxError => e Rails.logger.warn("[User#parsed_agent_conf] Invalid YAML for user #{id}: #{e.}") AGENT_CONF_DEFAULTS.deep_dup end |
#password_meets_minimum_length ⇒ Object
296 297 298 299 300 301 302 303 |
# File 'app/models/collavre/user.rb', line 296 def password_meets_minimum_length return if password.blank? min_length = Collavre::SystemSetting.password_min_length if password.length < min_length errors.add(:password, :too_short, count: min_length) end end |
#preserve_durable_summary_comments ⇒ Object
Keep durable compress/merge summaries (snapshot result comments) alive when their author is deleted: nullify authorship instead of cascading destroy.
315 316 317 318 319 |
# File 'app/models/collavre/user.rb', line 315 def preserve_durable_summary_comments Collavre::Comment .where(id: Collavre::CommentSnapshot.where(result_comment_id: comments.select(:id)).select(:result_comment_id)) .update_all(user_id: nil) end |
#record_failed_login! ⇒ Object
278 279 280 281 282 283 284 285 |
# File 'app/models/collavre/user.rb', line 278 def record_failed_login! new_count = (failed_login_attempts || 0) + 1 if new_count >= Collavre::SystemSetting.max_login_attempts update_columns(failed_login_attempts: new_count, locked_at: Time.current) else update_column(:failed_login_attempts, new_count) end end |
#remaining_lockout_time ⇒ Object
291 292 293 294 |
# File 'app/models/collavre/user.rb', line 291 def remaining_lockout_time return 0 unless locked? ((locked_at + Collavre::SystemSetting.lockout_duration) - Time.current).to_i end |
#reset_failed_login_attempts! ⇒ Object
287 288 289 |
# File 'app/models/collavre/user.rb', line 287 def reset_failed_login_attempts! update_column(:failed_login_attempts, 0) if failed_login_attempts.to_i > 0 end |
#supports_session? ⇒ Boolean
Whether this agent uses stateful sessions (incremental messaging). nil in agent_conf = auto-detect via the AiClient adapter layer, which vendor engines register into (core never string-matches a vendor name).
137 138 139 140 141 142 143 |
# File 'app/models/collavre/user.rb', line 137 def supports_session? explicit = parsed_agent_conf.dig("session", "enabled") return ActiveModel::Type::Boolean.new.cast(explicit) unless explicit.nil? return false if llm_vendor.blank? Collavre::AiClient.vendor_supports_session?(llm_vendor) end |
#theme_accessibility ⇒ Object
305 306 307 308 309 310 311 |
# File 'app/models/collavre/user.rb', line 305 def theme_accessibility return if theme.blank? || %w[light dark].include?(theme) unless user_themes.exists?(id: theme) errors.add(:theme, "is invalid") end end |
#typo_correction_active_for?(device:, location:) ⇒ Boolean
2D gating: typo correction runs only when the master switch is on AND the originating typing device AND the input location are both enabled. Unknown device/location values are treated as disabled (fail closed).
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'app/models/collavre/user.rb', line 169 def typo_correction_active_for?(device:, location:) return false unless typo_correction_enabled device_on = case device.to_s when "voice" then typo_correction_on_voice when "soft_keyboard" then typo_correction_on_soft_keyboard when "physical_keyboard" then typo_correction_on_physical_keyboard else false end location_on = case location.to_s when "chat" then typo_correction_in_chat when "editor" then typo_correction_in_editor else false end device_on && location_on end |
#unlock_account! ⇒ Object
274 275 276 |
# File 'app/models/collavre/user.rb', line 274 def unlock_account! update_columns(locked_at: nil, failed_login_attempts: 0) end |