diff --git a/.cursor/rules/general-rules.mdc b/.cursor/rules/general-rules.mdc
index ea5fb59b..c0f68e48 100644
--- a/.cursor/rules/general-rules.mdc
+++ b/.cursor/rules/general-rules.mdc
@@ -11,8 +11,4 @@ alwaysApply: true
- Do not run `touch tmp/restart.txt`
- Do not run `rails credentials`
- Do not automatically run migrations
-- Do not create or use i18n methods or language files
-- Do not create new API routes
-- Write and run tests as you go to verify functionality in small pieces
-- Focus on simple solutions
-- Use `gpt-4o` model from OpenAI unless otherwise noted.
\ No newline at end of file
+- Do not create or use i18n methods or language files
\ No newline at end of file
diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc
index d2d00dcc..f575f27a 100644
--- a/.cursor/rules/project-conventions.mdc
+++ b/.cursor/rules/project-conventions.mdc
@@ -19,6 +19,7 @@ This rule serves as high-level documentation for how the Maybe codebase is struc
- Hotwire Turbo/Stimulus for SPA-like UI/UX
- TailwindCSS for styles
- Lucide Icons for icons
+ - OpenAI for AI chat
- Database: PostgreSQL
- Jobs: GoodJob
- External
@@ -50,6 +51,7 @@ This codebase adopts a "skinny controller, fat models" convention. Furthermore,
### Convention 3: Prefer server-side solutions over client-side solutions
- When possible, leverage Turbo frames over complex, JS-driven client-side solutions
+- Prefer query params for state over JS based solutions
- When writing a client-side solution, use Stimulus controllers and keep it simple!
- Especially when dealing with money and currencies, calculate + format server-side and then pass that to the client to display
- Keep client-side code for where it truly shines. For example, [bulk_select_controller.js](mdc:app/javascript/controllers/bulk_select_controller.js) is a case where server-side solutions would degrade the user experience significantly. When bulk-selecting entries, client-side solutions are the way to go and Stimulus provides the right toolset to achieve this.
@@ -77,9 +79,10 @@ Due to the open-source nature of this project, we have chosen Minitest + Fixture
- Always use Minitest and fixtures for testing.
- Keep fixtures to a minimum. Most models should have 2-3 fixtures maximum that represent the "base cases" for that model. "Edge cases" should be created on the fly, within the context of the test which it is needed.
- For tests that require a large number of fixture records to be created, use Rails helpers such as [entries_test_helper.rb](mdc:test/support/account/entries_test_helper.rb) to act as a "factory" for creating these. For a great example of this, check out [balance_calculator_test.rb](mdc:test/models/account/balance_calculator_test.rb)
+- Take a minimal approach to testing—only test the absolutely critical code paths that will significantly increase developer confidence
### Convention 7: Use ActiveRecord for complex validations, DB for simple ones, keep business logic out of DB
- Enforce `null` checks, unique indexes, and other simple validations in the DB
- ActiveRecord validations _may_ mirror the DB level ones, but not 100% necessary. These are for convenience when error handling in forms. Always prefer client-side form validation when possible.
-- Complex validations and business logic should remain in ActiveRecord
\ No newline at end of file
+- Complex validations and business logic should remain in ActiveRecord
diff --git a/.cursor/rules/project-design.mdc b/.cursor/rules/project-design.mdc
index 8957f7a6..da2b8154 100644
--- a/.cursor/rules/project-design.mdc
+++ b/.cursor/rules/project-design.mdc
@@ -111,12 +111,12 @@ Below are brief descriptions of each type of sync in more detail.
### Account Syncs
-The most important type of sync is the account sync. It is orchestrated by the account [syncer.rb](mdc:app/models/account/syncer.rb), and performs a few important tasks:
+The most important type of sync is the account sync. It is orchestrated by the account's `sync_data` method, which performs a few important tasks:
- Auto-matches transfer records for the account
-- Calculates holdings and balances for the account
-- Enriches transaction data
-- Converts account balances that are not in the family's preferred currency to the preferred currency
+- Calculates daily [balance.rb](mdc:app/models/account/balance.rb) records for the account from `account.start_date` to `Date.current` using [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb)
+ - Balances are dependent on the calculation of [holding.rb](mdc:app/models/account/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/base_calculator.rb)
+- Enriches transaction data if enabled by user
An account sync happens every time an [entry.rb](mdc:app/models/account/entry.rb) is updated.
diff --git a/.cursor/rules/ui-ux-design-guidelines.mdc b/.cursor/rules/ui-ux-design-guidelines.mdc
index 7d5f7099..430959d6 100644
--- a/.cursor/rules/ui-ux-design-guidelines.mdc
+++ b/.cursor/rules/ui-ux-design-guidelines.mdc
@@ -3,12 +3,20 @@ description: This file describes Maybe's design system and how views should be s
globs: app/views/**,app/helpers/**,app/javascript/controllers/**
alwaysApply: true
---
-Use this rule whenever you are writing html, css, or even styles in Stimulus controllers that use D3.js.
+Use the rules below when:
+
+- You are writing HTML
+- You are writing CSS
+- You are writing styles in a JavaScript Stimulus controller
+
+## Rules for AI (mandatory)
The codebase uses TailwindCSS v4.x (the newest version) with a custom design system defined in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css)
-- Always start by referencing [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) to see the base primitives and tokens we use in the codebase
-- Always generate semantic HTML
+- Always start by referencing [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) to see the base primitives, functional tokens, and component tokens we use in the codebase
+- Always prefer using the functional "tokens" defined in @maybe-design-system.css when possible.
+ - Example 1: use `text-primary` rather than `text-gray-900`
+ - Example 2: use `bg-container` rather than `bg-white`
+ - Example 3: use `border border-primary` rather than `border border-gray-200`
- Never create new styles in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) or [application.css](mdc:app/assets/tailwind/application.css) without explicitly receiving permission to do so
-- Always favor the "utility first" Tailwind approach. Reusable style classes should not be created often. Code should be reused primarily through ERB partials.
-- Always prefer using the utility "tokens" defined in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) when possible. For example, use `text-primary` rather than `text-gray-900`.
\ No newline at end of file
+- Always generate semantic HTML
\ No newline at end of file
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d0ad6828..1f51c6e9 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
helper_method :require_upgrade?, :subscription_pending?
before_action :detect_os
- before_action :set_chat_for_sidebar
+ before_action :set_default_chat
private
def require_upgrade?
@@ -35,14 +35,9 @@ class ApplicationController < ActionController::Base
end
end
- def set_chat_for_sidebar
- return unless Current.user
- return unless params[:chat_id].present?
-
- @chat = Current.user.chats.find_by(id: params[:chat_id])
- if @chat
- @messages = @chat.messages.conversation.ordered
- @message = Message.new
- end
+ # By default, we show the user the last chat they interacted with
+ def set_default_chat
+ @last_viewed_chat = Current.user&.last_viewed_chat
+ @chat = @last_viewed_chat
end
end
diff --git a/app/controllers/chat_controller.js b/app/controllers/chat_controller.js
deleted file mode 100644
index 8dc2ced3..00000000
--- a/app/controllers/chat_controller.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Controller } from "@hotwired/stimulus"
-
-export default class extends Controller {
- static targets = ["messages", "form", "input"]
-
- connect() {
- this.scrollToBottom()
- this.setupAutoResize()
- this.setupMessageObserver()
- }
-
- scrollToBottom() {
- if (this.hasMessagesTarget) {
- this.messagesTarget.scrollTop = this.messagesTarget.scrollHeight
- }
- }
-
- setupAutoResize() {
- if (this.hasInputTarget) {
- this.inputTarget.addEventListener('input', this.autoResize.bind(this))
- // Initialize height
- this.autoResize()
- }
- }
-
- setupMessageObserver() {
- if (this.hasMessagesTarget) {
- // Create a mutation observer to watch for new messages
- this.observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.addedNodes.length) {
- this.scrollToBottom()
- }
- })
- })
-
- // Start observing
- this.observer.observe(this.messagesTarget, {
- childList: true,
- subtree: true
- })
- }
- }
-
- disconnect() {
- // Clean up observer when controller is disconnected
- if (this.observer) {
- this.observer.disconnect()
- }
- }
-
- autoResize() {
- const input = this.inputTarget
- // Reset height to calculate proper scrollHeight
- input.style.height = 'auto'
- // Set new height based on content
- input.style.height = (input.scrollHeight) + 'px'
- // Cap at 150px max height
- if (input.scrollHeight > 150) {
- input.style.height = '150px'
- input.style.overflowY = 'auto'
- } else {
- input.style.overflowY = 'hidden'
- }
- }
-
- submit(event) {
- // Let the form submit normally, but prepare for the response
- this.startLoadingState()
- }
-
- startLoadingState() {
- if (this.hasFormTarget) {
- this.formTarget.classList.add('opacity-50')
- this.formTarget.querySelector('button[type="submit"]').disabled = true
- }
- }
-
- endLoadingState() {
- if (this.hasFormTarget) {
- this.formTarget.classList.remove('opacity-50')
- this.formTarget.querySelector('button[type="submit"]').disabled = false
- this.formTarget.reset()
- this.autoResize()
- }
- }
-}
\ No newline at end of file
diff --git a/app/controllers/chats_controller.rb b/app/controllers/chats_controller.rb
index e91e79ef..f63de195 100644
--- a/app/controllers/chats_controller.rb
+++ b/app/controllers/chats_controller.rb
@@ -1,12 +1,16 @@
class ChatsController < ApplicationController
- before_action :set_chat, only: [ :show, :destroy, :clear ]
- before_action :ensure_ai_enabled, only: [ :create, :show ]
+ include ActionView::RecordIdentifier
+
+ before_action :set_chat, only: [ :show, :edit, :update, :destroy ]
+ before_action :ensure_ai_enabled, only: [ :edit, :update, :create, :show ]
def index
+ @chat = nil # override application_controller default behavior of setting @chat to last viewed chat
@chats = Current.user.chats.order(created_at: :desc)
end
def show
+ set_last_viewed_chat(@chat)
@messages = @chat.messages.conversation.ordered
@message = Message.new
@@ -16,10 +20,16 @@ class ChatsController < ApplicationController
end
end
+ def new
+ @chat = Current.user.chats.new(title: "New chat #{Time.current.strftime("%Y-%m-%d %H:%M")}")
+ end
+
def create
- @chat = Current.user.chats.new(title: "New Chat", user: Current.user)
+ @chat = Current.user.chats.new(chat_params.merge(title: "New Chat"))
if @chat.save
+ set_last_viewed_chat(@chat)
+
# Create initial system message with enhanced financial assistant context
@chat.messages.create(
content: "You are a helpful financial assistant for Maybe. You can answer questions about the user's finances including net worth, account balances, income, expenses, spending patterns, budgets, and financial goals. You have access to the user's financial data and can provide insights based on their transactions and accounts. Be conversational, helpful, and provide specific financial insights tailored to the user's question.",
@@ -31,7 +41,6 @@ class ChatsController < ApplicationController
@user_message = @chat.messages.create(
content: params[:content],
role: "user",
- user: Current.user
)
end
@@ -52,17 +61,41 @@ class ChatsController < ApplicationController
end
end
+ def edit
+ end
+
+ def update
+ @chat.update!(chat_params)
+
+ respond_to do |format|
+ format.html { redirect_back_or_to chat_path(@chat), notice: "Chat updated" }
+ format.turbo_stream { render turbo_stream: turbo_stream.replace(dom_id(@chat, :title), partial: "chats/chat_title", locals: { chat: @chat }) }
+ end
+ end
+
def destroy
@chat.destroy
+ clear_last_viewed_chat
redirect_to chats_path, notice: "Chat was successfully deleted"
end
private
-
def set_chat
@chat = Current.user.chats.find(params[:id])
end
+ def set_last_viewed_chat(chat)
+ Current.user.update!(last_viewed_chat: chat)
+ end
+
+ def clear_last_viewed_chat
+ Current.user.update!(last_viewed_chat: nil)
+ end
+
+ def chat_params
+ params.require(:chat).permit(:title, :content)
+ end
+
def ensure_ai_enabled
unless Current.user.ai_enabled?
redirect_to root_path, alert: "AI chat is not enabled. Please enable it in your settings."
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 4b114375..8688ee61 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -154,6 +154,13 @@ module ApplicationHelper
markdown.render(text).html_safe
end
+ def chat_view_path(chat)
+ return new_chat_path if params[:chat_view] == "new"
+ return chats_path if chat.nil? || params[:chat_view] == "all"
+
+ chat.persisted? ? chat_path(chat) : new_chat_path
+ end
+
private
def calculate_total(item, money_method, negate)
items = item.reject { |i| i.respond_to?(:entryable) && i.entryable.transfer? }
diff --git a/app/helpers/menus_helper.rb b/app/helpers/menus_helper.rb
index 55ad55a3..7f12b76e 100644
--- a/app/helpers/menus_helper.rb
+++ b/app/helpers/menus_helper.rb
@@ -1,13 +1,13 @@
module MenusHelper
- def contextual_menu(&block)
+ def contextual_menu(icon: "more-horizontal", &block)
tag.div data: { controller: "menu" } do
- concat contextual_menu_icon
+ concat contextual_menu_icon(icon)
concat contextual_menu_content(&block)
end
end
def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: :modal)
- link_to url, class: "flex items-center rounded-lg text-primary hover:bg-gray-50 py-2 px-3 gap-2", data: { turbo_frame: } do
+ link_to url, class: "flex items-center rounded-md text-primary hover:bg-container-hover p-2 gap-2", data: { action: "click->menu#close", turbo_frame: turbo_frame } do
concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-secondary"))
concat(tag.span(label, class: "text-sm"))
end
@@ -16,7 +16,7 @@ module MenusHelper
def contextual_menu_destructive_item(label, url, turbo_confirm: true, turbo_frame: nil)
button_to url,
method: :delete,
- class: "flex items-center w-full rounded-lg text-red-500 hover:bg-red-500/5 py-2 px-3 gap-2",
+ class: "flex items-center w-full rounded-md text-red-500 hover:bg-red-500/5 p-2 gap-2",
data: { turbo_confirm: turbo_confirm, turbo_frame: } do
concat(lucide_icon("trash-2", class: "shrink-0 w-5 h-5"))
concat(tag.span(label, class: "text-sm"))
@@ -24,14 +24,14 @@ module MenusHelper
end
private
- def contextual_menu_icon
- tag.button class: "flex hover:bg-gray-100 p-2 rounded cursor-pointer", data: { menu_target: "button" } do
- lucide_icon "more-horizontal", class: "w-5 h-5 text-secondary"
+ def contextual_menu_icon(icon)
+ tag.button class: "w-9 h-9 flex justify-center items-center hover:bg-surface-hover rounded-lg cursor-pointer focus:outline-none focus-visible:outline-none", data: { menu_target: "button" } do
+ lucide_icon icon, class: "w-5 h-5 text-secondary"
end
end
def contextual_menu_content(&block)
- tag.div class: "z-50 border border-alpha-black-25 bg-white rounded-lg shadow-xs hidden",
+ tag.div class: "min-w-[200px] p-1 z-50 shadow-border-xs bg-white rounded-lg hidden",
data: { menu_target: "content" } do
capture(&block)
end
diff --git a/app/jobs/process_ai_response_job.rb b/app/jobs/process_ai_response_job.rb
index f50b0045..5a0d7640 100644
--- a/app/jobs/process_ai_response_job.rb
+++ b/app/jobs/process_ai_response_job.rb
@@ -96,7 +96,7 @@ class ProcessAiResponseJob < ApplicationJob
target: "thinking",
html: <<~HTML
diff --git a/app/models/concerns/openai.rb b/app/models/concerns/openai.rb
deleted file mode 100644
index 0f4ed339..00000000
--- a/app/models/concerns/openai.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module OpenAI
- extend ActiveSupport::Concern
-
- class_methods do
- def openai_client
- api_key = ENV.fetch("OPENAI_ACCESS_TOKEN", Setting.openai_access_token)
-
- return nil unless api_key.present?
-
- OpenAI::Client.new(access_token: api_key)
- end
- end
-
- def openai_client
- self.class.openai_client
- end
-end
diff --git a/app/models/concerns/promptable.rb b/app/models/concerns/promptable.rb
index 3b8ba0a2..d1e43d60 100644
--- a/app/models/concerns/promptable.rb
+++ b/app/models/concerns/promptable.rb
@@ -1,6 +1,24 @@
module Promptable
extend ActiveSupport::Concern
+ # The openai ruby gem hasn't yet added support for the responses endpoint.
+ # TODO: Remove this once the gem implements it.
+ class CustomOpenAI < OpenAI::Client
+ def responses(parameters: {})
+ json_post(path: "/responses", parameters: parameters)
+ end
+ end
+
+ class_methods do
+ def openai_client
+ api_key = ENV.fetch("OPENAI_ACCESS_TOKEN", Setting.openai_access_token)
+
+ return nil unless api_key.present?
+
+ CustomOpenAI.new(access_token: api_key)
+ end
+ end
+
# Convert model data to a format that's readable by AI
def to_ai_readable_hash
raise NotImplementedError, "#{self.class} must implement to_ai_readable_hash"
@@ -29,6 +47,10 @@ module Promptable
private
+ def openai_client
+ self.class.openai_client
+ end
+
# Format currency values consistently for AI display
def format_currency(amount, currency = family.currency)
Money.new(amount, currency).format
diff --git a/app/models/message.rb b/app/models/message.rb
index 87d1ea8e..93bb1954 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -1,5 +1,5 @@
class Message < ApplicationRecord
- include OpenAI
+ include Promptable
belongs_to :chat
diff --git a/app/models/setting.rb b/app/models/setting.rb
index 41355fee..992717ba 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -18,6 +18,7 @@ class Setting < RailsSettings::Base
validates: { inclusion: { in: %w[release commit] } }
field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
+ field :openai_access_token, type: :string, default: ENV["OPENAI_ACCESS_TOKEN"]
field :require_invite_for_signup, type: :boolean, default: false
diff --git a/app/models/user.rb b/app/models/user.rb
index b1668054..d1aa9da2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,6 +2,7 @@ class User < ApplicationRecord
has_secure_password
belongs_to :family
+ belongs_to :last_viewed_chat, class_name: "Chat", optional: true
has_many :sessions, dependent: :destroy
has_many :chats, dependent: :destroy
has_many :impersonator_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonator_id, dependent: :destroy
diff --git a/app/views/layouts/shared/_ai_avatar.html.erb b/app/views/chats/_ai_avatar.html.erb
similarity index 100%
rename from app/views/layouts/shared/_ai_avatar.html.erb
rename to app/views/chats/_ai_avatar.html.erb
diff --git a/app/views/chats/_ai_consent.html.erb b/app/views/chats/_ai_consent.html.erb
new file mode 100644
index 00000000..37e913d8
--- /dev/null
+++ b/app/views/chats/_ai_consent.html.erb
@@ -0,0 +1,34 @@
+
+
+
+ <%= icon("sparkles") %>
+
+
+
Enable Personal Finance AI
+
+
+ <% if Current.user.ai_available? %>
+ Our personal finance AI can help answer questions about your finances and provide insights based on your data.
+ To use this feature, you'll need to explicitly enable it.
+ <% else %>
+ To use the AI assistant, you need to set the OPENAI_ACCESS_TOKEN
+ environment variable in your self-hosted instance.
+ <% end %>
+
+
+ <% unless self_hosted? %>
+
+ NOTE: During beta testing, we'll be monitoring usage and AI conversations to improve the assistant.
+
+
\ No newline at end of file
diff --git a/app/views/chats/_ai_greeting.html.erb b/app/views/chats/_ai_greeting.html.erb
new file mode 100644
index 00000000..ec210b07
--- /dev/null
+++ b/app/views/chats/_ai_greeting.html.erb
@@ -0,0 +1,29 @@
+
+ <%= render "chats/ai_avatar" %>
+
+
+
Hey <%= Current.user&.first_name || 'there' %>! I'm an AI built by Maybe to help with your finances. I have access to the web and your account data.
+
+
+ You can use / to access commands
+
+
+
+
Here's a few questions you can ask:
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/layouts/shared/_ai_sidebar.html.erb b/app/views/chats/_ai_sidebar.html.erb
similarity index 96%
rename from app/views/layouts/shared/_ai_sidebar.html.erb
rename to app/views/chats/_ai_sidebar.html.erb
index 85ea8e7b..e9ec2d3e 100644
--- a/app/views/layouts/shared/_ai_sidebar.html.erb
+++ b/app/views/chats/_ai_sidebar.html.erb
@@ -47,7 +47,7 @@
<% end %>
<% if !Current.user.ai_enabled? %>
- <%= render "layouts/shared/ai_consent" %>
+ <%= render "chats/ai_consent" %>
<% else %>
<%# Chat List Menu (hidden by default) %>
@@ -110,13 +110,13 @@
<%= render "messages/message", message: message %>
<% end %>
<% else %>
- <%= render "layouts/shared/ai_greeting", context: 'chat' %>
+ <%= render "chats/ai_greeting", context: 'chat' %>
<% end %>
<%# Thinking indicator - moved outside of messages div to prevent it from being replaced %>
-<% end %>
\ No newline at end of file
+<% end %>
\ No newline at end of file
diff --git a/app/views/chats/new.html.erb b/app/views/chats/new.html.erb
new file mode 100644
index 00000000..bf57c27d
--- /dev/null
+++ b/app/views/chats/new.html.erb
@@ -0,0 +1,9 @@
+<%= turbo_frame_tag "sidebar-chat-content" do %>
+
+ <%= render "chats/chat_nav", chat: @chat %>
+
+
+ <%= render "chats/ai_greeting" %>
+
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/chats/show.html.erb b/app/views/chats/show.html.erb
index 5d3cd063..824f3dc7 100644
--- a/app/views/chats/show.html.erb
+++ b/app/views/chats/show.html.erb
@@ -1,36 +1,19 @@
-<% content_for :title, @chat.title %>
+<%= turbo_frame_tag "sidebar-chat-content" do %>
+ <%= turbo_stream_from @chat %>
-<%= turbo_stream_from @chat %>
+
+ <%= render "chats/chat_nav", chat: @chat %>
-
-
<%= @chat.title %>
-
-
- <%= link_to chats_path, class: "py-2 px-4 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium" do %>
- All Chats
- <% end %>
-
- <%= button_to chat_path(@chat), method: :delete, class: "p-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200", data: { turbo_confirm: "Are you sure you want to delete this chat?" } do %>
- <%= icon("trash-2") %>
- <% end %>
-
-
-
-
-
-
+
<% if @messages.any? %>
<% @messages.each do |message| %>
<%= render "messages/message", message: message %>
<% end %>
<% else %>
-
- <%= render "layouts/shared/ai_greeting", context: 'chat' %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/chats/show.turbo_stream.erb b/app/views/chats/show.turbo_stream.erb
index f1ec5a32..8674851c 100644
--- a/app/views/chats/show.turbo_stream.erb
+++ b/app/views/chats/show.turbo_stream.erb
@@ -2,13 +2,13 @@
<% @messages.each do |message| %>
- <% if Current.user.ai_available? %>
- Our personal finance AI can help answer questions about your finances and provide insights based on your data.
- To use this feature, you'll need to explicitly enable it.
- <% else %>
- To use the AI assistant, you need to set the OPENAI_ACCESS_TOKEN
- environment variable in your self-hosted instance.
- <% end %>
-
-
-
- NOTE: During beta testing, we'll be monitoring usage and AI conversations to improve the assistant.
-
\ No newline at end of file
diff --git a/app/views/layouts/shared/_ai_greeting.html.erb b/app/views/layouts/shared/_ai_greeting.html.erb
deleted file mode 100644
index d0a42f1a..00000000
--- a/app/views/layouts/shared/_ai_greeting.html.erb
+++ /dev/null
@@ -1,75 +0,0 @@
-<%#
- This partial renders the AI greeting message.
-
- Parameters:
- - context: Either 'chat' (for existing chat) or 'default' (for new chat)
-%>
-
-<% context ||= 'default' %>
-
-
- <%= render "layouts/shared/ai_avatar" %>
-
- <% if context == 'chat' %>
-
Hey <%= Current.user&.first_name || 'there' %>! I'm your financial assistant. I can help you understand your finances by answering questions about your accounts, transactions, income, expenses, and net worth.
-
-
-
Here are some financial questions you can ask:
-
-
-
-
-
-
-
-
-
-
-
- <% else %>
-
Hey <%= Current.user&.first_name || 'there' %>! I'm an AI built by Maybe to help with your finances. I have access to the web and your account data.
-
-
- You can use / to access commands
-
-
-
-
Here's a few questions you can ask:
-
-
-
-
-
-
-
-
-
- <% end %>
-
-
\ No newline at end of file
diff --git a/app/views/messages/_form.html.erb b/app/views/messages/_form.html.erb
index 3779a61d..08da9b6b 100644
--- a/app/views/messages/_form.html.erb
+++ b/app/views/messages/_form.html.erb
@@ -1,68 +1,25 @@
-<%
-# This partial can be used in multiple contexts:
-# - In a chat show page: form_with model: [chat, message]
-# - In a new chat context: form_with url: chats_path, method: :post
-# - In a sidebar: with additional scroll behavior
-#
-# Parameters:
-# - chat: The chat object (optional, if creating a new chat)
-# - message: The message object (optional, if creating a new chat)
-# - form_options: Additional options for the form (default: {})
-# - scroll_behavior: Whether to include scroll behavior (default: false)
-%>
+<%# locals: (chat: nil) %>
-<%
- # Set up default values
- form_options ||= {}
- scroll_behavior ||= false
-
- # Determine form parameters based on context
- if defined?(chat) && chat.present? && defined?(message) && message.present?
- form_params = { model: [chat, message] }
- else
- form_params = { url: chats_path, method: :post }
- end
-
- # Merge form options
- form_params.merge!(form_options)
-
- # Set up controller and actions
- controller_data = { controller: "message-form", action: "turbo:submit-end->message-form#reset" }
- if scroll_behavior
- controller_data[:action] += " turbo:submit-end->chat-scroll#scrollToBottom"
- end
-%>
+<% model = chat ? [chat, Message.new] : Chat.new %>
-<%= form_with **form_params, class: "relative", data: controller_data do |f| %>
-
+ <%# These are disabled for now, but in the future, will all open specific menus with their own context and search %>
+ <% ["plus", "command", "at-sign", "mouse-pointer-click"].each do |icon| %>
+
+ <% end %>
+
+
<% end %>
\ No newline at end of file
diff --git a/app/views/messages/_message.html.erb b/app/views/messages/_message.html.erb
index f1aabbfd..9ffbaace 100644
--- a/app/views/messages/_message.html.erb
+++ b/app/views/messages/_message.html.erb
@@ -35,7 +35,7 @@