mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-04 21:15:19 +02:00
Personal finance AI (v1) (#2022)
* AI sidebar * Add chat and message models with associations * Implement AI chat functionality with sidebar and messaging system - Add chat and messages controllers - Create chat and message views - Implement chat-related routes - Add message broadcasting and user interactions - Update application layout to support chat sidebar - Enhance user model with initials method * Refactor AI sidebar with enhanced chat menu and interactions - Update sidebar layout with dynamic width and improved responsiveness - Add new chat menu Stimulus controller for toggling between chat and chat list views - Improve chat list display with recent chats and empty state - Extract AI avatar to a partial for reusability - Enhance message display and interaction styling - Add more contextual buttons and interaction hints * Improve chat scroll behavior and message styling - Refactor chat scroll functionality with Stimulus controller - Optimize message scrolling in chat views - Update message styling for better visual hierarchy - Enhance chat container layout with flex and auto-scroll - Simplify message rendering across different chat views * Extract AI avatar to a shared partial for consistent styling - Refactor AI avatar rendering across chat views - Replace hardcoded avatar markup with a reusable partial - Simplify avatar display in chats and messages views * Update sidebar controller to handle right panel width dynamically - Add conditional width class for right sidebar panel - Ensure consistent sidebar toggle behavior for both left and right panels - Use specific width class for right panel (w-[375px]) * Refactor chat form and AI greeting with flexible partials - Extract message form to a reusable partial with dynamic context support - Create flexible AI greeting partial for consistent welcome messages - Simplify chat and sidebar views by leveraging new partials - Add support for different form scenarios (chat, new chat, sidebar) - Improve code modularity and reduce duplication * Add chat clearing functionality with dynamic menu options - Implement clear chat action in ChatsController - Add clear chat route to support clearing messages - Update AI sidebar with dropdown menu for chat actions - Preserve system message when clearing chat - Enhance chat interaction with new menu options * Add frontmatter to project structure documentation - Create initial frontmatter for structure.mdc file - Include description and configuration options - Prepare for potential dynamic documentation rendering * Update general project rules with additional guidelines - Add rule for using `Current.family` instead of `current_family` - Include new guidelines for testing, API routes, and solution approach - Expand project-specific rules for more consistent development practices * Add OpenAI gem and AI-friendly data representations - Add `ruby-openai` gem for AI integration - Implement `to_ai_readable_hash` methods in BalanceSheet and IncomeStatement - Include Promptable module in both models - Add savings rate calculation method in IncomeStatement - Prepare financial models for AI-powered insights and interactions * Enhance AI Financial Assistant with Advanced Querying and Debugging Capabilities - Implement comprehensive AI financial query system with function-based interactions - Add detailed debug logging for AI responses and function calls - Extend BalanceSheet and IncomeStatement models with AI-friendly methods - Create robust error handling and fallback mechanisms for AI queries - Update chat and message views to support debug mode and enhanced rendering - Add AI query routes and initial test coverage for financial assistant * Refactor AI sidebar and chat layout with improved structure and comments - Remove inline AI chat from application layout - Enhance AI sidebar with more semantic HTML structure - Add descriptive comments to clarify different sections of chat view - Improve flex layout and scrolling behavior in chat messages container - Optimize message rendering with more explicit class names and structure * Add Markdown rendering support for AI chat messages - Implement `markdown` helper method in ApplicationHelper using Redcarpet - Update message view to render AI messages with Markdown formatting - Add comprehensive Markdown rendering options (tables, code blocks, links) - Enhance AI Financial Assistant prompt to encourage Markdown usage - Remove commented Markdown CSS in Tailwind application stylesheet * Missing comma * Enhance AI response processing with chat history context * Improve AI debug logging with payload size limits and internal message flag * Enhance AI chat interaction with improved thinking indicator and scrolling behavior * Add AI consent and enable/disable functionality for AI chat * Upgrade Biome and refactor JavaScript template literals - Update @biomejs/biome to latest version with caret (^) notation - Refactor AI query and chat controllers to use template literals - Standardize npm scripts formatting in package.json * Add beta testing usage note to AI consent modal * Update test fixtures and configurations for AI chat functionality - Add family association to chat fixtures and tests - Set consistent password digest for test users - Enable AI for test users - Add OpenAI access token for test environment - Update chat and user model tests to include family context * Simplify data model and get tests passing * Remove structure.mdc from version control * Integrate AI chat styles into existing prose pattern * Match Figma design spec, implement Turbo frames and actions for chats controller * AI rules refresh * Consolidate Stimulus controllers, thinking state, controllers, and views * Naming, domain alignment * Reset migrations * Improve data model to support tool calls and message types * Tool calling tests and fixtures * Tool call implementation and test * Get assistant test working again * Test updates * Process tool calls within provider * Chat UI back to working state again * Remove stale code * Tests passing * Update openai class naming to avoid conflicts * Reconfigure test env * Rebuild gemfile * Fix naming conflicts for ChatResponse * Message styles * Use OpenAI conversation state management * Assistant function base implementation * Add back thinking messages, clean up error handling for chat * Fix sync error when security price has bad data from provider * Add balance sheet function to assistant * Add better function calling error visibility * Add income statement function * Simplify and clean up "thinking" interactions with Turbo frames * Remove stale data definitions from functions * Ensure VCR fixtures working with latest code * basic stream implementation * Get streaming working * Make AI sidebar wider when left sidebar is collapsed * Get tests working with streaming responses * Centralize provider error handling * Provider data boundaries --------- Co-authored-by: Josh Pigford <josh@joshpigford.com>
This commit is contained in:
parent
8e6b81af77
commit
2f6b11c18f
126 changed files with 3576 additions and 462 deletions
|
@ -1,6 +1,6 @@
|
|||
<%# locals: (family:) %>
|
||||
|
||||
<% if family.requires_data_provider? && Providers.synth.nil? %>
|
||||
<% if family.requires_data_provider? && Provider::Registry.get_provider(:synth).nil? %>
|
||||
<details class="group bg-yellow-tint-10 rounded-lg p-2 text-yellow-600 mb-3 text-xs">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
23
app/views/assistant_messages/_assistant_message.html.erb
Normal file
23
app/views/assistant_messages/_assistant_message.html.erb
Normal file
|
@ -0,0 +1,23 @@
|
|||
<%# locals: (assistant_message:) %>
|
||||
|
||||
<div id="<%= dom_id(assistant_message) %>">
|
||||
<% if assistant_message.reasoning? %>
|
||||
<details class="group mb-1">
|
||||
<summary class="flex items-center gap-2">
|
||||
<p class="text-secondary text-sm">Assistant reasoning</p>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-secondary w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="prose prose--ai-chat"><%= markdown(assistant_message.content) %></div>
|
||||
</details>
|
||||
<% else %>
|
||||
<% if assistant_message.chat.debug_mode? && assistant_message.tool_calls.any? %>
|
||||
<%= render "assistant_messages/tool_calls", message: assistant_message %>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-start mb-6">
|
||||
<%= render "chats/ai_avatar" %>
|
||||
<div class="prose prose--ai-chat"><%= markdown(assistant_message.content) %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
19
app/views/assistant_messages/_tool_calls.html.erb
Normal file
19
app/views/assistant_messages/_tool_calls.html.erb
Normal file
|
@ -0,0 +1,19 @@
|
|||
<%# locals: (message:) %>
|
||||
|
||||
<details class="my-2 group mb-4">
|
||||
<summary class="text-secondary text-xs cursor-pointer flex items-center gap-2">
|
||||
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-secondary w-5" %>
|
||||
<p>Tool Calls</p>
|
||||
</summary>
|
||||
|
||||
<div class="mt-2">
|
||||
<% message.tool_calls.each do |tool_call| %>
|
||||
<div class="bg-blue-50 border-blue-200 px-3 py-2 rounded-lg border mb-2">
|
||||
<p class="text-secondary text-xs">Function:</p>
|
||||
<p class="text-primary text-sm font-mono"><%= tool_call.function_name %></p>
|
||||
<p class="text-secondary text-xs mt-2">Arguments:</p>
|
||||
<pre class="text-primary text-sm font-mono whitespace-pre-wrap"><%= tool_call.function_arguments %></pre>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
3
app/views/chats/_ai_avatar.html.erb
Normal file
3
app/views/chats/_ai_avatar.html.erb
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="w-16 h-16 flex-shrink-0 -ml-3 -mt-3">
|
||||
<%= image_tag "ai.svg", alt: "AI", class: "w-full h-full" %>
|
||||
</div>
|
33
app/views/chats/_ai_consent.html.erb
Normal file
33
app/views/chats/_ai_consent.html.erb
Normal file
|
@ -0,0 +1,33 @@
|
|||
<div class="flex flex-col items-center justify-start h-full p-6 text-center">
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-white">
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<%= icon("sparkles") %>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Enable Personal Finance AI</h3>
|
||||
|
||||
<p class="text-gray-600 mb-6 text-sm">
|
||||
<% 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 <code class="bg-gray-100 px-1 py-0.5 rounded font-mono text-xs">OPENAI_ACCESS_TOKEN</code>
|
||||
environment variable in your self-hosted instance.
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<% unless self_hosted? %>
|
||||
<p class="text-gray-600 mb-6 text-sm">
|
||||
NOTE: During beta testing, we'll be monitoring usage and AI conversations to improve the assistant.
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% if Current.user.ai_available? %>
|
||||
<%= form_with url: user_path(Current.user), method: :patch, class: "w-full", data: { turbo: false } do |form| %>
|
||||
<%= form.hidden_field "user[ai_enabled]", value: true %>
|
||||
<%= form.hidden_field "user[redirect_to]", value: "home" %>
|
||||
<%= form.submit "Enable AI Assistant", class: "cursor-pointer hover:bg-black w-full py-2 px-4 bg-gray-800 text-white rounded-lg text-sm font-medium" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
40
app/views/chats/_ai_greeting.html.erb
Normal file
40
app/views/chats/_ai_greeting.html.erb
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div class="flex items-start gap-2 w-full">
|
||||
<%= render "chats/ai_avatar" %>
|
||||
|
||||
<div class="max-w-[85%] text-sm space-y-4 text-primary">
|
||||
<p>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.</p>
|
||||
|
||||
<p>
|
||||
You can use <span class="bg-white border border-gray-200 px-1.5 py-0.5 rounded font-mono text-xs">/</span> to access commands
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<p>Here's a few questions you can ask:</p>
|
||||
|
||||
<% questions = [
|
||||
{
|
||||
icon: "bar-chart-2",
|
||||
text: "Evaluate investment portfolio"
|
||||
},
|
||||
{
|
||||
icon: "credit-card",
|
||||
text: "Show spending insights"
|
||||
},
|
||||
{
|
||||
icon: "alert-triangle",
|
||||
text: "Find unusual patterns"
|
||||
}
|
||||
] %>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<% questions.each do |question| %>
|
||||
<button data-action="chat#submitSampleQuestion"
|
||||
data-chat-question-param="<%= question[:text] %>"
|
||||
class="w-full flex items-center gap-2 border border-tertiary rounded-full py-1.5 px-2.5 hover:bg-gray-100">
|
||||
<%= icon(question[:icon]) %> <%= question[:text] %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
16
app/views/chats/_chat.html.erb
Normal file
16
app/views/chats/_chat.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<%# locals: (chat:) %>
|
||||
|
||||
<%= tag.div class: "flex items-center justify-between px-4 py-3 bg-container shadow-border-xs rounded-lg" do %>
|
||||
<div class="grow">
|
||||
<%= render "chats/chat_title", chat: chat, ctx: "list" %>
|
||||
|
||||
<p class="text-sm text-secondary">
|
||||
<%= time_ago_in_words(chat.updated_at) %> ago
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= contextual_menu icon: "more-vertical" do %>
|
||||
<%= contextual_menu_item("Edit chat", url: edit_chat_path(chat), icon: "pencil", turbo_frame: dom_id(chat, :title)) %>
|
||||
<%= contextual_menu_destructive_item("Delete chat", chat_path(chat)) %>
|
||||
<% end %>
|
||||
<% end %>
|
24
app/views/chats/_chat_nav.html.erb
Normal file
24
app/views/chats/_chat_nav.html.erb
Normal file
|
@ -0,0 +1,24 @@
|
|||
<%# locals: (chat:) %>
|
||||
|
||||
<nav class="flex items-center justify-between">
|
||||
<% path = chat.new_record? ? chats_path : chats_path(previous_chat_id: chat.id) %>
|
||||
|
||||
<div class="flex items-center gap-2 grow">
|
||||
<%= link_to path, id: "chat-nav-back", class: "w-9 h-9 flex items-center justify-center rounded-lg hover:bg-surface-hover" do %>
|
||||
<%= icon("menu", color: "gray" ) %>
|
||||
<% end %>
|
||||
|
||||
<div class="grow">
|
||||
<%= render "chats/chat_title", chat: chat, ctx: "chat" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= contextual_menu icon: "more-vertical", id: "chat-menu" do %>
|
||||
<%= contextual_menu_item "Start new chat", url: new_chat_path, icon: "plus" %>
|
||||
|
||||
<% unless chat.new_record? %>
|
||||
<%= contextual_menu_item "Edit chat title", url: edit_chat_path(chat, ctx: "chat"), icon: "pencil", turbo_frame: dom_id(chat, "title") %>
|
||||
<%= contextual_menu_destructive_item "Delete chat", chat_path(chat), turbo_confirm: "Are you sure you want to delete this chat?" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</nav>
|
11
app/views/chats/_chat_title.html.erb
Normal file
11
app/views/chats/_chat_title.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%# locals: (chat:, ctx: "list") %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(chat, :title), class: "block" do %>
|
||||
<% if chat.new_record? || ctx == "chat" %>
|
||||
<h3 class="text-sm font-medium text-primary"><%= chat.title || "New chat" %></h3>
|
||||
<% else %>
|
||||
<%= link_to chat_path(chat), data: { turbo_frame: chat_frame } do %>
|
||||
<h3 class="truncate text-sm font-medium text-primary"><%= chat.title %></h3>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
17
app/views/chats/_error.html.erb
Normal file
17
app/views/chats/_error.html.erb
Normal file
|
@ -0,0 +1,17 @@
|
|||
<%# locals: (chat:) %>
|
||||
|
||||
<div id="chat-error" class="px-3 py-2 bg-red-100 border border-red-500 rounded-lg">
|
||||
<% if chat.debug_mode? %>
|
||||
<div class="overflow-x-auto text-xs p-4 bg-red-200 rounded-md mb-2">
|
||||
<code><%= chat.error %></code>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<p class="text-xs text-red-500">Failed to generate response. Please try again.</p>
|
||||
|
||||
<%= button_to retry_chat_path(chat), method: :post, class: "btn btn--primary" do %>
|
||||
<span>Retry</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
6
app/views/chats/_thinking_indicator.html.erb
Normal file
6
app/views/chats/_thinking_indicator.html.erb
Normal file
|
@ -0,0 +1,6 @@
|
|||
<%# locals: (chat:, message: "Thinking ...") -%>
|
||||
|
||||
<div id="thinking-indicator" class="flex items-start">
|
||||
<%= render "chats/ai_avatar" %>
|
||||
<p class="text-sm text-secondary animate-pulse"><%= message %></p>
|
||||
</div>
|
8
app/views/chats/edit.html.erb
Normal file
8
app/views/chats/edit.html.erb
Normal file
|
@ -0,0 +1,8 @@
|
|||
<%= turbo_frame_tag dom_id(@chat, :title), class: "block" do %>
|
||||
<% bg_class = params[:ctx] == "chat" ? "bg-white" : "bg-container-inset" %>
|
||||
<%= styled_form_with model: @chat,
|
||||
class: class_names("p-1 rounded-md font-medium text-primary w-full", bg_class),
|
||||
data: { controller: "auto-submit-form", auto_submit_form_trigger_event_value: "blur" } do |f| %>
|
||||
<%= f.text_field :title, data: { auto_submit_form_target: "auto" }, inline: true %>
|
||||
<% end %>
|
||||
<% end %>
|
31
app/views/chats/index.html.erb
Normal file
31
app/views/chats/index.html.erb
Normal file
|
@ -0,0 +1,31 @@
|
|||
<%= turbo_frame_tag chat_frame do %>
|
||||
<div class="p-4 flex flex-col h-full">
|
||||
<nav class="mb-6">
|
||||
<% back_path = @last_viewed_chat ? chat_path(@last_viewed_chat) : new_chat_path %>
|
||||
<%= link_to back_path, class: "w-9 h-9 flex items-center justify-center rounded-lg hover:bg-surface-hover" do %>
|
||||
<%= icon("arrow-left", color: "gray" ) %>
|
||||
<% end %>
|
||||
</nav>
|
||||
|
||||
<div class="grow">
|
||||
<h1 class="text-xl font-medium mb-6">Chats</h1>
|
||||
|
||||
<% if @chats.any? %>
|
||||
<div class="space-y-2 px-0.5">
|
||||
<%= render @chats %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12 bg-white rounded-lg border border-gray-200">
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<%= icon("message-square", size: "lg") %>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-1">No chats yet</h3>
|
||||
<p class="text-gray-500 mb-4">Start a new conversation with the AI assistant</p>
|
||||
<%= link_to "Start a chat", new_chat_path, class: "inline-flex items-center gap-2 py-2 px-4 bg-gray-800 text-white rounded-lg text-sm font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render "messages/chat_form" %>
|
||||
</div>
|
||||
<% end %>
|
11
app/views/chats/new.html.erb
Normal file
11
app/views/chats/new.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%= turbo_frame_tag chat_frame do %>
|
||||
<div class="p-4 flex flex-col h-full">
|
||||
<%= render "chats/chat_nav", chat: @chat %>
|
||||
|
||||
<div class="mt-auto py-8">
|
||||
<%= render "chats/ai_greeting" %>
|
||||
</div>
|
||||
|
||||
<%= render "messages/chat_form", chat: @chat %>
|
||||
</div>
|
||||
<% end %>
|
35
app/views/chats/show.html.erb
Normal file
35
app/views/chats/show.html.erb
Normal file
|
@ -0,0 +1,35 @@
|
|||
<%= turbo_frame_tag chat_frame do %>
|
||||
<%= turbo_stream_from @chat %>
|
||||
|
||||
<h1 class="sr-only"><%= @chat.title %></h1>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="p-4">
|
||||
<%= render "chats/chat_nav", chat: @chat %>
|
||||
</div>
|
||||
|
||||
<div id="messages" class="grow overflow-y-auto p-4 space-y-6" data-chat-target="messages">
|
||||
<% if @chat.conversation_messages.any? %>
|
||||
<% @chat.conversation_messages.ordered.each do |message| %>
|
||||
<%= render message %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="mt-auto">
|
||||
<%= render "chats/ai_greeting", context: "chat" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if params[:thinking].present? %>
|
||||
<%= render "chats/thinking_indicator", chat: @chat %>
|
||||
<% end %>
|
||||
|
||||
<% if @chat.error.present? %>
|
||||
<%= render "chats/error", chat: @chat %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<%= render "messages/chat_form", chat: @chat %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
6
app/views/developer_messages/_developer_message.html.erb
Normal file
6
app/views/developer_messages/_developer_message.html.erb
Normal file
|
@ -0,0 +1,6 @@
|
|||
<%# locals: (developer_message:) %>
|
||||
|
||||
<div id="<%= dom_id(developer_message) %>" class="my-2 <%= developer_message.debug? ? "bg-yellow-50 border-yellow-200" : "bg-blue-50 border-blue-200" %> px-3 py-2 rounded-lg max-w-[85%] ml-auto border">
|
||||
<span class="text-secondary text-xs"><%= developer_message.debug? ? "Debug message (internal only)" : "System instruction (sent to AI)" %></span>
|
||||
<p class="text-primary text-sm"><%= developer_message.content %></p>
|
||||
</div>
|
|
@ -1,5 +1,10 @@
|
|||
<%= render "layouts/shared/htmldoc" do %>
|
||||
<div class="flex h-full bg-gray-50">
|
||||
<% sidebar_config = app_sidebar_config(Current.user) %>
|
||||
|
||||
<div class="flex h-full bg-gray-50"
|
||||
data-controller="sidebar"
|
||||
data-sidebar-user-id-value="<%= Current.user.id %>"
|
||||
data-sidebar-config-value="<%= sidebar_config.to_json %>">
|
||||
<nav class="flex flex-col shrink-0 w-[84px] py-4 mr-3">
|
||||
<div class="pl-2 mb-3">
|
||||
<%= link_to root_path, class: "block" do %>
|
||||
|
@ -26,7 +31,9 @@
|
|||
</div>
|
||||
</nav>
|
||||
|
||||
<%= tag.div class: class_names("py-4 shrink-0 h-full overflow-y-auto transition-all duration-300", Current.user.show_sidebar? ? "w-80" : "w-0"), data: { sidebar_target: "panel" } do %>
|
||||
<%= tag.div class: class_names("py-4 shrink-0 h-full overflow-y-auto transition-all duration-300"),
|
||||
style: "width: #{sidebar_config.dig(:left_panel, :initial_width)}px",
|
||||
data: { sidebar_target: "leftPanel" } do %>
|
||||
<% if content_for?(:sidebar) %>
|
||||
<%= yield :sidebar %>
|
||||
<% else %>
|
||||
|
@ -43,7 +50,7 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= tag.div class: class_names("mx-auto w-full h-full", Current.user.show_sidebar? ? "max-w-4xl" : "max-w-5xl"), data: { sidebar_target: "content" } do %>
|
||||
<%= tag.div style: "max-width: #{sidebar_config.dig(:content_max_width)}px", class: class_names("mx-auto w-full h-full"), data: { sidebar_target: "content" } do %>
|
||||
<% if content_for?(:breadcrumbs) %>
|
||||
<%= yield :breadcrumbs %>
|
||||
<% else %>
|
||||
|
@ -57,5 +64,22 @@
|
|||
<%= yield %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%# AI chat sidebar %>
|
||||
<%= tag.div id: "chat-container",
|
||||
style: "width: #{sidebar_config.dig(:right_panel, :initial_width)}px",
|
||||
class: class_names("flex flex-col justify-between shrink-0 transition-all duration-300"),
|
||||
data: { controller: "chat hotkey", sidebar_target: "rightPanel", turbo_permanent: true } do %>
|
||||
|
||||
<% if Current.user.ai_enabled? %>
|
||||
<%= turbo_frame_tag chat_frame, src: chat_view_path(@chat), loading: "lazy", class: "h-full" do %>
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<%= lucide_icon("loader-circle", class: "w-5 h-5 text-secondary animate-spin") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= render "chats/ai_consent" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<nav class="flex items-center gap-2 mb-6">
|
||||
<% if sidebar_toggle_enabled %>
|
||||
<button data-action="sidebar#toggle" class="p-2 inline-flex rounded-lg items-center justify-center hover:bg-gray-100 cursor-pointer">
|
||||
<button data-action="sidebar#toggleLeftPanel" class="p-2 inline-flex rounded-lg items-center justify-center hover:bg-gray-100 cursor-pointer">
|
||||
<%= icon("panel-left", color: "gray") %>
|
||||
</button>
|
||||
<% end %>
|
||||
|
@ -22,4 +22,12 @@
|
|||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if sidebar_toggle_enabled %>
|
||||
<div class="ml-auto">
|
||||
<button data-action="sidebar#toggleRightPanel" class="p-2 inline-flex rounded-lg items-center justify-center hover:bg-gray-100 cursor-pointer" title="Toggle AI Assistant">
|
||||
<%= icon("panel-right", color: "gray") %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</nav>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
<%= csp_meta_tag %>
|
||||
|
||||
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
|
||||
<%= javascript_include_tag "https://cdn.plaid.com/link/v2/stable/link-initialize.js" %>
|
||||
<%= combobox_style_tag %>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<%= yield :head %>
|
||||
</head>
|
||||
|
||||
<body class="h-full antialiased" data-controller="sidebar" data-sidebar-user-id-value="<%= Current.user&.id %>">
|
||||
<body class="h-full antialiased">
|
||||
<div class="fixed z-50 bottom-6 left-24 w-80">
|
||||
<div id="notification-tray" class="space-y-1 w-full">
|
||||
<%= render_flash_notifications %>
|
||||
|
|
35
app/views/messages/_chat_form.html.erb
Normal file
35
app/views/messages/_chat_form.html.erb
Normal file
|
@ -0,0 +1,35 @@
|
|||
<%# locals: (chat: nil, message_hint: nil) %>
|
||||
|
||||
<div id="chat-form" class="space-y-2">
|
||||
<% model = chat && chat.persisted? ? [chat, Message.new] : Chat.new %>
|
||||
|
||||
<%= form_with model: model,
|
||||
class: "flex flex-col gap-2 bg-white px-2 py-1.5 rounded-lg shadow-border-xs",
|
||||
data: { chat_target: "form" } do |f| %>
|
||||
|
||||
<%# In the future, this will be a dropdown with different AI models %>
|
||||
<%= f.hidden_field :ai_model, value: "gpt-4o" %>
|
||||
|
||||
<%= f.text_area :content, placeholder: "Ask anything ...", value: message_hint,
|
||||
class: "w-full border-0 focus:ring-0 text-sm resize-none px-1",
|
||||
data: { chat_target: "input", action: "input->chat#autoResize keydown->chat#handleInputKeyDown" },
|
||||
rows: 1 %>
|
||||
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<%# 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| %>
|
||||
<button type="button" title="Coming soon" class="cursor-not-allowed w-8 h-8 flex justify-center items-center hover:bg-surface-hover rounded-lg">
|
||||
<%= icon(icon, color: "gray") %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-8 h-8 flex justify-center items-center text-secondary hover:bg-surface-hover cursor-pointer rounded-lg">
|
||||
<%= icon("arrow-up") %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<p class="text-xs text-secondary">AI responses are informational only and are not financial advice.</p>
|
||||
</div>
|
|
@ -5,7 +5,7 @@
|
|||
<%= combobox_security.name.presence || combobox_security.symbol %>
|
||||
</span>
|
||||
<span class="text-xs text-secondary">
|
||||
<%= "#{combobox_security.symbol} (#{combobox_security.exchange_acronym.presence || combobox_security.exchange_operating_mic})" %>
|
||||
<%= "#{combobox_security.symbol} (#{combobox_security.exchange_operating_mic})" %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-primary bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= contextual_menu_modal_action_item t(".edit_categories"), categories_path, icon: "shapes", turbo_frame: :_top %>
|
||||
<%= contextual_menu_modal_action_item t(".edit_tags"), tags_path, icon: "tags", turbo_frame: :_top %>
|
||||
<%= contextual_menu_modal_action_item t(".edit_merchants"), merchants_path, icon: "store", turbo_frame: :_top %>
|
||||
<%= contextual_menu_modal_action_item t(".edit_imports"), imports_path, icon: "hard-drive-upload", turbo_frame: :_top %>
|
||||
</div>
|
||||
<%= contextual_menu_modal_action_item t(".edit_categories"), categories_path, icon: "shapes", turbo_frame: :_top %>
|
||||
<%= contextual_menu_modal_action_item t(".edit_tags"), tags_path, icon: "tags", turbo_frame: :_top %>
|
||||
<%= contextual_menu_modal_action_item t(".edit_merchants"), merchants_path, icon: "store", turbo_frame: :_top %>
|
||||
<%= contextual_menu_modal_action_item t(".edit_imports"), imports_path, icon: "hard-drive-upload", turbo_frame: :_top %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_import_path, class: "btn btn--outline flex items-center gap-2", data: { turbo_frame: "modal" } do %>
|
||||
|
|
5
app/views/user_messages/_user_message.html.erb
Normal file
5
app/views/user_messages/_user_message.html.erb
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%# locals: (user_message:) %>
|
||||
|
||||
<div id="<%= dom_id(user_message) %>" class="bg-gray-100 px-3 py-2 rounded-lg max-w-[85%] w-fit ml-auto mb-6">
|
||||
<div class="prose prose--ai-chat"><%= markdown(user_message.content) %></div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue