Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ class PagesController < ApplicationController
skip_authentication only: :redis_configuration_error

def dashboard
if Current.user&.ui_layout_intro?
redirect_to chats_path and return
end

@balance_sheet = Current.family.balance_sheet
@accounts = Current.family.accounts.visible.with_attached_logo

Expand Down Expand Up @@ -45,6 +49,10 @@ def dashboard
@breadcrumbs = [ [ "Home", root_path ], [ "Dashboard", nil ] ]
end

def intro
@breadcrumbs = [ [ "Home", chats_path ], [ "Intro", nil ] ]
end

def changelog
@release_notes = github_provider.fetch_latest_release_notes

Expand Down
8 changes: 7 additions & 1 deletion app/controllers/settings/profiles_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Settings::ProfilesController < ApplicationController
layout "settings"
layout :layout_for_settings_profile

def show
@user = Current.user
Expand Down Expand Up @@ -36,4 +36,10 @@ def destroy

redirect_to settings_profile_path
end

private

def layout_for_settings_profile
Current.user&.ui_layout_intro? ? "application" : "settings"
end
end
46 changes: 42 additions & 4 deletions app/models/assistant/configurable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,51 @@ def config_for(chat)
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,
Expand Down
18 changes: 18 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ 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 :ui_layout, { dashboard: "dashboard", intro: "intro" }, validate: true, prefix: true

before_validation :apply_ui_layout_defaults, on: :create

has_one_attached :profile_image do |attachable|
attachable.variant :thumbnail, resize_to_fill: [ 300, 300 ], convert: :webp, saver: { quality: 80 }
Expand Down Expand Up @@ -96,6 +99,11 @@ def ai_enabled?
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

# Deactivation
validate :can_deactivate, if: -> { active_changed? && !active }
after_update_commit :purge_later, if: -> { saved_change_to_active?(from: true, to: false) }
Expand Down Expand Up @@ -170,6 +178,16 @@ def account_order
end

private
def apply_ui_layout_defaults
self.ui_layout = (ui_layout.presence || self.class.default_ui_layout)

if ui_layout_intro?
self.show_sidebar = false
self.show_ai_sidebar = false
self.ai_enabled = true
end
end

def ensure_valid_profile_image
return unless profile_image.attached?

Expand Down
66 changes: 41 additions & 25 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
<% mobile_nav_items = [
{ name: "Home", path: root_path, icon: "pie-chart", icon_custom: false, active: page_active?(root_path) },
{ name: "Transactions", path: transactions_path, icon: "credit-card", icon_custom: false, active: page_active?(transactions_path) },
{ name: "Budgets", path: budgets_path, icon: "map", icon_custom: false, active: page_active?(budgets_path) },
{ name: "Assistant", path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
] %>
<% intro_mode = Current.user&.ui_layout_intro? %>
<% home_path = intro_mode ? chats_path : root_path %>
<% mobile_nav_items = if intro_mode
[
{ name: "Home", path: chats_path, icon: "home", icon_custom: false, active: page_active?(chats_path) },
{ name: "Intro", path: intro_path, icon: "sparkles", icon_custom: false, active: page_active?(intro_path) }
]
else
[
{ name: "Home", path: root_path, icon: "pie-chart", icon_custom: false, active: page_active?(root_path) },
{ name: "Transactions", path: transactions_path, icon: "credit-card", icon_custom: false, active: page_active?(transactions_path) },
{ name: "Budgets", path: budgets_path, icon: "map", icon_custom: false, active: page_active?(budgets_path) },
{ name: "Assistant", path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
]
end %>

<% desktop_nav_items = mobile_nav_items.reject { |item| item[:mobile_only] } %>
<% expanded_sidebar_class = "w-full" %>
Expand All @@ -19,9 +28,11 @@
<div
class="hidden fixed inset-0 bg-surface z-20 h-full w-full pt-[calc(env(safe-area-inset-top)+0.75rem)] pr-3 pb-[calc(env(safe-area-inset-bottom)+0.75rem)] pl-3 overflow-y-auto transition-all duration-300"
data-app-layout-target="mobileSidebar">
<div class="mb-2">
<%= icon("x", as_button: true, data: { action: "app-layout#closeMobileSidebar" }) %>
</div>
<% unless intro_mode %>
<div class="mb-2">
<%= icon("x", as_button: true, data: { action: "app-layout#closeMobileSidebar" }) %>
</div>
<% end %>

<%= render(
"accounts/account_sidebar_tabs",
Expand All @@ -33,20 +44,23 @@

<%# MOBILE - Top nav %>
<nav class="lg:hidden flex justify-between items-center p-3">
<%= icon("panel-left", as_button: true, data: { action: "app-layout#openMobileSidebar"}) %>
<% if intro_mode %>
<% else %>
<%= icon("panel-left", as_button: true, data: { action: "app-layout#openMobileSidebar"}) %>
<% end %>

<%= link_to root_path, class: "block" do %>
<%= link_to home_path, class: "block" do %>
<%= image_tag "logomark-color.svg", class: "w-9 h-9 mx-auto" %>
<% end %>

<%= render "users/user_menu", user: Current.user, placement: "bottom-end", offset: 12 %>
<%= render "users/user_menu", user: Current.user, placement: "bottom-end", offset: 12, intro_mode: intro_mode %>
</nav>

<%# DESKTOP - Left navbar %>
<div class="hidden lg:block">
<nav class="h-full flex flex-col shrink-0 w-[84px] py-4 mr-3">
<div class="pl-2 mb-3">
<%= link_to root_path, class: "block" do %>
<%= link_to home_path, class: "block" do %>
<%= image_tag "logomark-color.svg", class: "w-9 h-9 mx-auto" %>
<% end %>
</div>
Expand All @@ -67,7 +81,7 @@
target: "_blank"
) %>

<%= render "users/user_menu", user: Current.user %>
<%= render "users/user_menu", user: Current.user, intro_mode: intro_mode %>
</div>
</nav>
</div>
Expand Down Expand Up @@ -112,18 +126,20 @@

<%# SHARED - Main content %>
<%= tag.main class: class_names("grow overflow-y-auto px-3 lg:px-10 py-4 w-full mx-auto max-w-5xl"), data: { app_layout_target: "content" } do %>
<div class="hidden lg:flex gap-2 items-center justify-between mb-6">
<div class="flex items-center gap-2">
<%= icon("panel-left", as_button: true, data: { action: "app-layout#toggleLeftSidebar" }) %>

<% if content_for?(:breadcrumbs) %>
<%= yield :breadcrumbs %>
<% else %>
<%= render "layouts/shared/breadcrumbs", breadcrumbs: @breadcrumbs %>
<% end %>
<% unless intro_mode %>
<div class="hidden lg:flex gap-2 items-center justify-between mb-6">
<div class="flex items-center gap-2">
<%= icon("panel-left", as_button: true, data: { action: "app-layout#toggleLeftSidebar" }) %>

<% if content_for?(:breadcrumbs) %>
<%= yield :breadcrumbs %>
<% else %>
<%= render "layouts/shared/breadcrumbs", breadcrumbs: @breadcrumbs %>
<% end %>
</div>
<%= icon("panel-right", as_button: true, data: { action: "app-layout#toggleRightSidebar" }) %>
</div>
<%= icon("panel-right", as_button: true, data: { action: "app-layout#toggleRightSidebar" }) %>
</div>
<% end %>

<% if content_for?(:page_header) %>
<%= yield :page_header %>
Expand Down
21 changes: 21 additions & 0 deletions app/views/pages/intro.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<% content_for :page_header do %>
<div class="space-y-2">
<h1 class="text-2xl font-semibold text-primary">Welcome!</h1>
<br/>
</div>
<% end %>

<div class="mx-auto max-w-3xl">
<div class="bg-container shadow-border-xs rounded-2xl p-8 text-center space-y-4">
<div class="flex justify-center">
<%= image_tag "logomark-color.svg", class: "w-16 h-16" %>
</div>
<h2 class="text-xl font-semibold text-primary">Intro experience coming soon</h2>
<p class="text-secondary">
We're building a richer onboarding journey to learn about your goals, milestones, and day-to-day needs. For now, head over to the chat sidebar to start a conversation with Sure and let us know where you are in your financial journey.
</p>
<div>
<%= link_to "Start chatting", chats_path, class: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-white font-medium" %>
</div>
</div>
</div>
Loading