Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
12 changes: 12 additions & 0 deletions app/controllers/settings/ai_prompts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Settings::AiPromptsController < ApplicationController
layout "settings"

def show
@breadcrumbs = [
[ "Home", root_path ],
[ "AI Prompts", nil ]
]
@family = Current.family
@assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing require for OpenStruct may raise NameError in production

OpenStruct is in Ruby’s stdlib but not auto-required. Without require "ostruct", constants may be unavailable depending on boot order.

Apply this diff:

+require "ostruct"
 class Settings::AiPromptsController < ApplicationController
   layout "settings"

Alternatively (no stdlib dependency):

-    @assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
+    ChatContext = Struct.new(:user)
+    @assistant_config = Assistant.config_for(ChatContext.new(Current.user))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
require "ostruct"
class Settings::AiPromptsController < ApplicationController
layout "settings"
def show
@assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
end
end
🤖 Prompt for AI Agents
In app/controllers/settings/ai_prompts_controller.rb around line 10, the code
uses OpenStruct but does not require the stdlib, which can raise NameError in
some boot orders; either add require "ostruct" at the top of the file (or in an
initializer) so OpenStruct is always loaded, or refactor to avoid the stdlib
dependency (e.g., replace OpenStruct.new(user: Current.user) with a simple PORO
or Struct.new(:user).new(Current.user)) to ensure the constant is available in
production.

end
end
2 changes: 1 addition & 1 deletion app/controllers/settings/api_keys_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Settings::ApiKeysController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "API Keys", nil ]
[ "API Key", nil ]
]
@current_api_key = @api_key
end
Expand Down
18 changes: 18 additions & 0 deletions app/controllers/settings/guides_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Settings::GuidesController < ApplicationController
layout "settings"

def show
@breadcrumbs = [
[ "Home", root_path ],
[ "Guides", nil ]
]
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML,
autolink: true,
tables: true,
fenced_code_blocks: true,
strikethrough: true,
superscript: true
)
@guide_content = markdown.render(File.read(Rails.root.join("docs/onboarding/guide.md")))
end
end
4 changes: 4 additions & 0 deletions app/controllers/settings/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ def show
@user = Current.user
@users = Current.family.users.order(:created_at)
@pending_invitations = Current.family.invitations.pending
@breadcrumbs = [
[ "Home", root_path ],
[ "Profile Info", nil ]
]
end

def destroy
Expand Down
20 changes: 14 additions & 6 deletions app/helpers/settings_helper.rb
Copy link

@matthieuEv matthieuEv Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the order, i think it would be better to have:

  • Profil Info
  • Preferences
  • Security
  • Accounts
  • Bank Sync

This order look a lot more like some other settings, with the "User related" settings in first

Also the SETTINGS_ORDER do not seems to be used in the Settings Section Sidebar, i think it would be best to use one var to manage this, for readability

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so also maybe changing the object like:

SETTINGS_ORDER = [
  "GENERAL" : [
    { name: "Profile Info", path: :settings_profile_path },
    { name: "Preferences", path: :settings_preferences_path },
    { name: "Accounts", path: :accounts_path },
    { name: "Bank Sync", path: :settings_bank_sync_path },
    { name: "Security", path: :settings_security_path },
  ],
  "TRANSACTIONS" : [
    { name: "Billing", path: :settings_billing_path, condition: :not_self_hosted? },
    { name: "Categories", path: :categories_path },
    { name: "Tags", path: :tags_path },
    { name: "Rules", path: :rules_path },
    { name: "Merchants", path: :family_merchants_path },
  ],
  "ADVANCED" : [
    { name: "AI Prompts", path: :settings_ai_prompts_path },
    { name: "API Key", path: :settings_api_key_path },
    { name: "Self-Hosting", path: :settings_hosting_path, condition: :self_hosted? },
    { name: "Imports", path: :imports_path },
    { name: "SimpleFin", path: :simplefin_items_path },
  ],
  "MORE" : [
    { name: "Guides", path: :settings_guides_path },
    { name: "What's new", path: :changelog_path },
    { name: "Feedback", path: :feedback_path }
  ]
]

Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
module SettingsHelper
SETTINGS_ORDER = [
{ name: "Account", path: :settings_profile_path },
# General section
{ name: "Accounts", path: :accounts_path },
{ name: "Bank Sync", path: :settings_bank_sync_path },
{ name: "Preferences", path: :settings_preferences_path },
{ name: "Profile Info", path: :settings_profile_path },
{ name: "Security", path: :settings_security_path },
{ name: "Self-Hosting", path: :settings_hosting_path, condition: :self_hosted? },
{ name: "API Key", path: :settings_api_key_path },
{ name: "Billing", path: :settings_billing_path, condition: :not_self_hosted? },
{ name: "Accounts", path: :accounts_path },
{ name: "Imports", path: :imports_path },
{ name: "Tags", path: :tags_path },
# Transactions section
{ name: "Categories", path: :categories_path },
{ name: "Tags", path: :tags_path },
{ name: "Rules", path: :rules_path },
{ name: "Merchants", path: :family_merchants_path },
# Advanced section
{ name: "AI Prompts", path: :settings_ai_prompts_path },
{ name: "API Key", path: :settings_api_key_path },
{ name: "Self-Hosting", path: :settings_hosting_path, condition: :self_hosted? },
{ name: "Imports", path: :imports_path },
{ name: "SimpleFin", path: :simplefin_items_path },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why having a SimpleFin section as it is already in the Bank Sync?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan is for this to go away indeed, once #104 goes in we can take of it (just wanted to avoid the merge conflict!)

# More section
{ name: "Guides", path: :settings_guides_path },
{ name: "What's new", path: :changelog_path },
{ name: "Feedback", path: :feedback_path }
]
Expand Down
42 changes: 21 additions & 21 deletions app/models/provider/openai/auto_categorizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ def auto_categorize
build_response(extract_categorizations(response))
end

def instructions
<<~INSTRUCTIONS.strip_heredoc
You are an assistant to a consumer personal finance app. You will be provided a list
of the user's transactions and a list of the user's categories. Your job is to auto-categorize
each transaction.

Closely follow ALL the rules below while auto-categorizing:

- Return 1 result per transaction
- Correlate each transaction by ID (transaction_id)
- Attempt to match the most specific category possible (i.e. subcategory over parent category)
- Category and transaction classifications should match (i.e. if transaction is an "expense", the category must have classification of "expense")
- If you don't know the category, return "null"
- You should always favor "null" over false positives
- Be slightly pessimistic. Only match a category if you're 60%+ confident it is the correct one.
- Each transaction has varying metadata that can be used to determine the category
- Note: "hint" comes from 3rd party aggregators and typically represents a category name that
may or may not match any of the user-supplied categories
INSTRUCTIONS
end

private
attr_reader :client, :model, :transactions, :user_categories

Expand Down Expand Up @@ -97,25 +118,4 @@ def developer_message
```
MESSAGE
end

def instructions
<<~INSTRUCTIONS.strip_heredoc
You are an assistant to a consumer personal finance app. You will be provided a list
of the user's transactions and a list of the user's categories. Your job is to auto-categorize
each transaction.

Closely follow ALL the rules below while auto-categorizing:

- Return 1 result per transaction
- Correlate each transaction by ID (transaction_id)
- Attempt to match the most specific category possible (i.e. subcategory over parent category)
- Category and transaction classifications should match (i.e. if transaction is an "expense", the category must have classification of "expense")
- If you don't know the category, return "null"
- You should always favor "null" over false positives
- Be slightly pessimistic. Only match a category if you're 60%+ confident it is the correct one.
- Each transaction has varying metadata that can be used to determine the category
- Note: "hint" comes from 3rd party aggregators and typically represents a category name that
may or may not match any of the user-supplied categories
INSTRUCTIONS
end
end
82 changes: 41 additions & 41 deletions app/models/provider/openai/auto_merchant_detector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,47 @@ def auto_detect_merchants
build_response(extract_categorizations(response))
end

def instructions
<<~INSTRUCTIONS.strip_heredoc
You are an assistant to a consumer personal finance app.

Closely follow ALL the rules below while auto-detecting business names and website URLs:

- Return 1 result per transaction
- Correlate each transaction by ID (transaction_id)
- Do not include the subdomain in the business_url (i.e. "amazon.com" not "www.amazon.com")
- User merchants are considered "manual" user-generated merchants and should only be used in 100% clear cases
- Be slightly pessimistic. We favor returning "null" over returning a false positive.
- NEVER return a name or URL for generic transaction names (e.g. "Paycheck", "Laundromat", "Grocery store", "Local diner")

Determining a value:

- First attempt to determine the name + URL from your knowledge of global businesses
- If no certain match, attempt to match one of the user-provided merchants
- If no match, return "null"

Example 1 (known business):

```
Transaction name: "Some Amazon purchases"

Result:
- business_name: "Amazon"
- business_url: "amazon.com"
```

Example 2 (generic business):

```
Transaction name: "local diner"

Result:
- business_name: null
- business_url: null
```
INSTRUCTIONS
end

private
attr_reader :client, :model, :transactions, :user_merchants

Expand Down Expand Up @@ -103,45 +144,4 @@ def developer_message
Return "null" if you are not 80%+ confident in your answer.
MESSAGE
end

def instructions
<<~INSTRUCTIONS.strip_heredoc
You are an assistant to a consumer personal finance app.

Closely follow ALL the rules below while auto-detecting business names and website URLs:

- Return 1 result per transaction
- Correlate each transaction by ID (transaction_id)
- Do not include the subdomain in the business_url (i.e. "amazon.com" not "www.amazon.com")
- User merchants are considered "manual" user-generated merchants and should only be used in 100% clear cases
- Be slightly pessimistic. We favor returning "null" over returning a false positive.
- NEVER return a name or URL for generic transaction names (e.g. "Paycheck", "Laundromat", "Grocery store", "Local diner")

Determining a value:

- First attempt to determine the name + URL from your knowledge of global businesses
- If no certain match, attempt to match one of the user-provided merchants
- If no match, return "null"

Example 1 (known business):

```
Transaction name: "Some Amazon purchases"

Result:
- business_name: "Amazon"
- business_url: "amazon.com"
```

Example 2 (generic business):

```
Transaction name: "local diner"

Result:
- business_name: null
- business_url: null
```
INSTRUCTIONS
end
end
25 changes: 16 additions & 9 deletions app/views/settings/_settings_nav.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,37 @@ nav_sections = [
{
header: t(".general_section_title"),
items: [
{ label: t(".profile_label"), path: settings_profile_path, icon: "circle-user" },
{ label: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" },
{ label: t(".security_label"), path: settings_security_path, icon: "shield-check" },
{ label: t(".api_keys_label"), path: settings_api_key_path, icon: "key" },
{ label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? },
{ label: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign", if: !self_hosted? },
{ label: t(".accounts_label"), path: accounts_path, icon: "layers" },
{ label: t(".bank_sync_label"), path: settings_bank_sync_path, icon: "banknote" },
{ label: "SimpleFin", path: simplefin_items_path, icon: "building-2" },
{ label: t(".imports_label"), path: imports_path, icon: "download" }
{ label: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" },
{ label: t(".profile_label"), path: settings_profile_path, icon: "circle-user" },
{ label: t(".security_label"), path: settings_security_path, icon: "shield-check" },
{ label: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign", if: !self_hosted? }
]
},
{
header: t(".transactions_section_title"),
items: [
{ label: t(".tags_label"), path: tags_path, icon: "tags" },
{ label: t(".categories_label"), path: categories_path, icon: "shapes" },
{ label: t(".tags_label"), path: tags_path, icon: "tags" },
{ label: t(".rules_label"), path: rules_path, icon: "git-branch" },
{ label: t(".merchants_label"), path: family_merchants_path, icon: "store" }
]
},
{
header: t(".advanced_section_title"),
items: [
{ label: t(".ai_prompts_label"), path: settings_ai_prompts_path, icon: "bot" },
{ label: t(".api_keys_label"), path: settings_api_key_path, icon: "key" },
{ label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? },
{ label: t(".imports_label"), path: imports_path, icon: "download" },
{ label: "SimpleFin", path: simplefin_items_path, icon: "building-2" }
]
},
{
header: t(".other_section_title"),
items: [
{ label: t(".guides_label"), path: settings_guides_path, icon: "book-open" },
{ label: t(".whats_new_label"), path: changelog_path, icon: "box" },
{ label: t(".feedback_label"), path: feedback_path, icon: "megaphone" }
]
Expand Down
55 changes: 55 additions & 0 deletions app/views/settings/ai_prompts/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<%= content_for :page_title, "AI Prompts" %>

<div class="bg-container rounded-xl shadow-border-xs p-4">
<div class="rounded-xl bg-container-inset space-y-1 p-1">
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-secondary">
<p>OpenAI</p>
</div>

<div class="bg-container rounded-lg shadow-border-xs">
<div class="space-y-4 p-4">
<!-- Auto-Categorization Section -->
<div class="space-y-3">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
<%= icon "brain" %>
</div>
<div>
<p class="text-sm font-medium text-primary">Transaction Categorizer</p>
<p class="text-xs text-secondary">AI automatically categorizes your transactions based on your defined categories</p>
</div>
</div>

<div class="pl-12 space-y-2">
<label class="text-xs font-medium text-primary uppercase">Instructions</label>
<div class="px-3 py-2 bg-surface-default border border-primary rounded-lg">
<pre class="whitespace-pre-wrap text-xs font-mono text-primary"><%= @assistant_config[:auto_categorizer]&.instructions || Provider::Openai::AutoCategorizer.new(nil).instructions %></pre>
</div>
</div>
</div>

<div class="border-t border-primary"></div>

<!-- Merchant Detection Section -->
<div class="space-y-3">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
<%= icon "store" %>
</div>
<div>
<p class="text-sm font-medium text-primary">Merchant Detector</p>
<p class="text-xs text-secondary">AI identifies and enriches transaction data with merchant information</p>
</div>
</div>

<div class="pl-12 space-y-2">
<label class="text-xs font-medium text-primary uppercase">Instructions</label>
<div class="px-3 py-2 bg-surface-default border border-primary rounded-lg">
<pre class="whitespace-pre-wrap text-xs font-mono text-primary"><%= @assistant_config[:auto_merchant]&.instructions || Provider::Openai::AutoMerchantDetector.new(nil, model: "", transactions: [], user_merchants: []).instructions %></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
4 changes: 2 additions & 2 deletions app/views/settings/api_keys/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<%= content_for :page_title, "Create New API Key" %>

<%= settings_section title: "Create New API Key", subtitle: "Generate a new API key to access your Maybe data programmatically." do %>
<%= settings_section title: nil, subtitle: "Generate a new API key to access your Maybe data programmatically." do %>
<%= styled_form_with model: @api_key, url: settings_api_key_path, class: "space-y-4" do |form| %>
<%= form.text_field :name,
placeholder: "e.g., My Budget App, Portfolio Tracker",
Expand Down Expand Up @@ -51,7 +51,7 @@
) %>

<%= render DS::Button.new(
text: "Create API Key",
text: "Save API Key",
variant: "primary",
type: "submit"
) %>
Expand Down
Loading