First cut of a simplified "intro" UI layout (#265)

* First cut of a simplified "intro" UI layout

* Linter

* Add guest role and intro-only access

* Fix guest role UI defaults (#940)

Use enum predicate to avoid missing role helper.

* Remove legacy user role mapping (#941)

Drop the unused user role references in role normalization
and SSO role mapping forms to avoid implying a role that
never existed.

Refs: #0

* Remove role normalization (#942)

Remove role normalization

Roles are now stored directly without legacy mappings.

* Revert role mapping logic

* Remove `normalize_role_settings`

* Remove unnecessary migration

* Make `member` the default

* Broken `.erb`

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
Juan José Mata
2026-02-09 11:09:25 +01:00
committed by GitHub
parent ba442d5f26
commit 705b5a8b26
33 changed files with 556 additions and 138 deletions

View File

@@ -6,13 +6,51 @@ module Assistant::Configurable
preferred_currency = Money::Currency.new(chat.user.family.currency)
preferred_date_format = chat.user.family.date_format
{
instructions: default_instructions(preferred_currency, preferred_date_format),
functions: default_functions
}
if chat.user.ui_layout_intro?
{
instructions: intro_instructions(preferred_currency, preferred_date_format),
functions: []
}
else
{
instructions: default_instructions(preferred_currency, preferred_date_format),
functions: default_functions
}
end
end
private
def intro_instructions(preferred_currency, preferred_date_format)
<<~PROMPT
## Your identity
You are Sure, a warm and curious financial guide welcoming a new household to the Sure personal finance application.
## Your purpose
Host an introductory conversation that helps you understand the user's stage of life, financial responsibilities, and near-term priorities so future guidance feels personal and relevant.
## Conversation approach
- Ask one thoughtful question at a time and tailor follow-ups based on what the user shares.
- Reflect key details back to the user to confirm understanding.
- Keep responses concise, friendly, and free of filler phrases.
- If the user requests detailed analytics, let them know the dashboard experience will cover it soon and guide them back to sharing context.
## Information to uncover
- Household composition and stage of life milestones (education, career, retirement, dependents, caregiving, etc.).
- Primary financial goals, concerns, and timelines.
- Notable upcoming events or obligations.
## Formatting guidelines
- Use markdown for any lists or emphasis.
- When money or timeframes are discussed, format currency with #{preferred_currency.symbol} (#{preferred_currency.iso_code}) and dates using #{preferred_date_format}.
- Do not call external tools or functions.
PROMPT
end
def default_functions
[
Assistant::Function::GetTransactions,

View File

@@ -171,7 +171,7 @@ class Demo::Generator
onboarded_at: onboarded ? Time.current : nil
)
# Member user
# Family member user
family.users.create!(
email: "partner_#{email}",
first_name: "Eve",

View File

@@ -11,7 +11,7 @@ class Invitation < ApplicationRecord
end
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :role, presence: true, inclusion: { in: %w[admin member] }
validates :role, presence: true, inclusion: { in: %w[admin member guest] }
validates :token, presence: true, uniqueness: true
validates_uniqueness_of :email, scope: :family_id, message: "has already been invited to this family"
validate :inviter_is_admin
@@ -32,7 +32,7 @@ class Invitation < ApplicationRecord
return false unless emails_match?(user)
transaction do
user.update!(family_id: family_id, role: role)
user.update!(family_id: family_id, role: role.to_s)
update!(accepted_at: Time.current)
end
true

View File

@@ -104,11 +104,12 @@ class SsoProvider < ApplicationRecord
end
def validate_default_role_setting
default_role = settings&.dig("default_role")
default_role = settings&.dig("default_role") || settings&.dig(:default_role)
default_role = default_role.to_s
return if default_role.blank?
unless User.roles.key?(default_role)
errors.add(:settings, "default_role must be member, admin, or super_admin")
errors.add(:settings, "default_role must be guest, member, admin, or super_admin")
end
end

View File

@@ -50,7 +50,11 @@ class User < ApplicationRecord
normalizes :first_name, :last_name, with: ->(value) { value.strip.presence }
enum :role, { member: "member", admin: "admin", super_admin: "super_admin" }, validate: true
enum :role, { guest: "guest", member: "member", admin: "admin", super_admin: "super_admin" }, validate: true
enum :ui_layout, { dashboard: "dashboard", intro: "intro" }, validate: true, prefix: true
before_validation :apply_ui_layout_defaults
before_validation :apply_role_based_ui_defaults
# Returns the appropriate role for a new user creating a family.
# The very first user of an instance becomes super_admin; subsequent users
@@ -139,6 +143,11 @@ class User < ApplicationRecord
ai_enabled && ai_available?
end
def self.default_ui_layout
layout = Rails.application.config.x.ui&.default_layout || "dashboard"
layout.in?(%w[intro dashboard]) ? layout : "dashboard"
end
# SSO-only users have OIDC identities but no local password.
# They cannot use password reset or local login.
def sso_only?
@@ -307,6 +316,39 @@ class User < ApplicationRecord
end
private
def apply_ui_layout_defaults
self.ui_layout = (ui_layout.presence || self.class.default_ui_layout)
end
def apply_role_based_ui_defaults
if ui_layout_intro?
if guest?
self.show_sidebar = false
self.show_ai_sidebar = false
self.ai_enabled = true
else
self.ui_layout = "dashboard"
end
elsif guest?
self.ui_layout = "intro"
self.show_sidebar = false
self.show_ai_sidebar = false
self.ai_enabled = true
end
if leaving_guest_role?
self.show_sidebar = true unless show_sidebar
self.show_ai_sidebar = true unless show_ai_sidebar
end
end
def leaving_guest_role?
return false unless will_save_change_to_role?
previous_role, new_role = role_change_to_be_saved
previous_role == "guest" && new_role != "guest"
end
def skip_password_validation?
skip_password_validation == true
end