mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-10 07:55:21 +02:00
Enhance AI chat interaction with improved thinking indicator and scrolling behavior
This commit is contained in:
parent
7d0591fb30
commit
64e7183e4d
9 changed files with 347 additions and 109 deletions
|
@ -31,3 +31,26 @@
|
|||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Thinking indicator styles */
|
||||
#thinking {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
z-index: 10; /* Ensure it's above other elements */
|
||||
position: relative; /* Needed for z-index to work */
|
||||
}
|
||||
|
||||
#thinking.hidden {
|
||||
display: none !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#thinking .animate-bounce {
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/* Ensure the thinking indicator is always visible */
|
||||
#thinking.flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
|
87
app/javascript/controllers/chat_progress_controller.js
Normal file
87
app/javascript/controllers/chat_progress_controller.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
/**
|
||||
* A controller to handle AI progress updates in the chat interface
|
||||
*/
|
||||
export default class extends Controller {
|
||||
static targets = ["thinking"]
|
||||
|
||||
connect() {
|
||||
console.log("ChatProgressController connected")
|
||||
this.setupProgressObserver()
|
||||
|
||||
// Check if the thinking indicator is already visible
|
||||
if (this.hasThinkingTarget && !this.thinkingTarget.classList.contains('hidden')) {
|
||||
console.log("Thinking indicator is already visible on connect")
|
||||
this.scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
setupProgressObserver() {
|
||||
if (this.hasThinkingTarget) {
|
||||
console.log("Setting up progress observer for thinking target")
|
||||
|
||||
// Create a mutation observer to watch for changes to the thinking indicator
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
this.handleThinkingVisibilityChange()
|
||||
} else if (mutation.type === 'childList') {
|
||||
this.handleThinkingContentChange()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Start observing
|
||||
this.observer.observe(this.thinkingTarget, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
} else {
|
||||
console.warn("No thinking target found")
|
||||
|
||||
// Try to find the thinking element by ID as a fallback
|
||||
const thinkingElement = document.getElementById('thinking')
|
||||
if (thinkingElement) {
|
||||
console.log("Found thinking element by ID")
|
||||
this.thinkingTarget = thinkingElement
|
||||
this.setupProgressObserver()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleThinkingVisibilityChange() {
|
||||
const isHidden = this.thinkingTarget.classList.contains('hidden')
|
||||
console.log("Thinking visibility changed:", isHidden ? "hidden" : "visible")
|
||||
|
||||
if (!isHidden) {
|
||||
// Scroll to the bottom when thinking indicator becomes visible
|
||||
this.scrollToBottom()
|
||||
|
||||
// Force a redraw to ensure the indicator is visible
|
||||
void this.thinkingTarget.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
handleThinkingContentChange() {
|
||||
console.log("Thinking content changed")
|
||||
// Scroll to the bottom when thinking indicator content changes
|
||||
this.scrollToBottom()
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
const messagesContainer = document.querySelector("[data-chat-scroll-target='messages']")
|
||||
if (messagesContainer) {
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,10 @@ export default class extends Controller {
|
|||
static targets = ["container", "messages"]
|
||||
|
||||
connect() {
|
||||
console.log("ChatScrollController connected")
|
||||
this.scrollToBottom()
|
||||
this.setupMessageObserver()
|
||||
this.setupThinkingObserver()
|
||||
|
||||
// Add event listener for manual scrolling (to detect if user has scrolled up)
|
||||
if (this.hasContainerTarget) {
|
||||
|
@ -20,10 +22,12 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
if (this.messageObserver) {
|
||||
this.messageObserver.disconnect()
|
||||
}
|
||||
if (this.thinkingObserver) {
|
||||
this.thinkingObserver.disconnect()
|
||||
}
|
||||
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
|
@ -34,62 +38,84 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
scrollToBottom() {
|
||||
console.log("Scrolling to bottom")
|
||||
if (this.hasContainerTarget) {
|
||||
const container = this.containerTarget
|
||||
container.scrollTop = container.scrollHeight
|
||||
this.containerTarget.scrollTop = this.containerTarget.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
if (this.hasContainerTarget) {
|
||||
const container = this.containerTarget
|
||||
const scrollPosition = container.scrollTop + container.clientHeight
|
||||
const scrollThreshold = container.scrollHeight - 50
|
||||
const isScrolledToBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50
|
||||
|
||||
// If user has scrolled up significantly, we'll track that
|
||||
if (scrollPosition < scrollThreshold) {
|
||||
this.userHasScrolled = true
|
||||
} else {
|
||||
this.userHasScrolled = false
|
||||
}
|
||||
// Update userHasScrolled state based on scroll position
|
||||
this.userHasScrolled = !isScrolledToBottom
|
||||
|
||||
console.log("User has scrolled:", this.userHasScrolled)
|
||||
}
|
||||
}
|
||||
|
||||
setupResizeObserver() {
|
||||
if (this.hasContainerTarget) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
// Only auto-scroll to bottom if the user hasn't manually scrolled up
|
||||
if (!this.userHasScrolled) {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
})
|
||||
|
||||
this.resizeObserver.observe(this.containerTarget)
|
||||
}
|
||||
}
|
||||
|
||||
setupMessageObserver() {
|
||||
// Create a mutation observer to watch for new messages
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
let shouldScroll = false
|
||||
if (this.hasMessagesTarget) {
|
||||
console.log("Setting up message observer")
|
||||
// Create a mutation observer to watch for new messages
|
||||
this.messageObserver = new MutationObserver((mutations) => {
|
||||
let shouldScroll = false
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length) {
|
||||
shouldScroll = true
|
||||
}
|
||||
})
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length) {
|
||||
shouldScroll = true
|
||||
if (shouldScroll && !this.userHasScrolled) {
|
||||
// Use setTimeout to ensure DOM is fully updated before scrolling
|
||||
setTimeout(() => this.scrollToBottom(), 0)
|
||||
}
|
||||
})
|
||||
|
||||
if (shouldScroll && !this.userHasScrolled) {
|
||||
// Use setTimeout to ensure DOM is fully updated before scrolling
|
||||
setTimeout(() => this.scrollToBottom(), 50)
|
||||
}
|
||||
})
|
||||
// Start observing
|
||||
this.messageObserver.observe(this.messagesTarget, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Start observing the messages container and its children
|
||||
const targetNode = this.hasMessagesTarget ? this.messagesTarget : this.containerTarget
|
||||
this.observer.observe(targetNode, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
setupThinkingObserver() {
|
||||
// Watch for changes to the thinking indicator
|
||||
const thinkingElement = document.getElementById('thinking')
|
||||
if (thinkingElement) {
|
||||
console.log("Setting up thinking observer")
|
||||
this.thinkingObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
const isHidden = thinkingElement.classList.contains('hidden')
|
||||
console.log("Thinking visibility changed:", isHidden ? "hidden" : "visible")
|
||||
|
||||
if (!isHidden && !this.userHasScrolled) {
|
||||
// Scroll to bottom when thinking indicator becomes visible
|
||||
setTimeout(() => this.scrollToBottom(), 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Start observing
|
||||
this.thinkingObserver.observe(thinkingElement, {
|
||||
attributes: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,10 +14,8 @@ export default class extends Controller {
|
|||
this.element.reset()
|
||||
this.element.querySelector("textarea").style.height = "auto"
|
||||
|
||||
// Hide the thinking indicator if it's visible
|
||||
if (this.thinkingElement) {
|
||||
this.thinkingElement.classList.add("hidden")
|
||||
}
|
||||
// We don't hide the thinking indicator here anymore
|
||||
// It will be hidden by the ProcessAiResponseJob when the AI response is ready
|
||||
}
|
||||
|
||||
checkSubmit(event) {
|
||||
|
@ -41,13 +39,18 @@ export default class extends Controller {
|
|||
console.log("Showing thinking indicator")
|
||||
this.thinkingElement.classList.remove("hidden")
|
||||
|
||||
// Scroll to the bottom of the chat
|
||||
const chatMessages = document.getElementById("chat-messages")
|
||||
// Force a redraw to ensure the indicator is visible
|
||||
void this.thinkingElement.offsetHeight;
|
||||
|
||||
// Scroll to the bottom of the chat to show the thinking indicator
|
||||
const chatMessages = document.querySelector("[data-chat-scroll-target='messages']")
|
||||
if (chatMessages) {
|
||||
setTimeout(() => {
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight
|
||||
}, 100)
|
||||
}
|
||||
} else {
|
||||
console.warn("Thinking element not found")
|
||||
}
|
||||
|
||||
console.log("Submit target:", this.submitTarget)
|
||||
|
|
|
@ -14,39 +14,101 @@ class ProcessAiResponseJob < ApplicationJob
|
|||
chat.update(title: new_title)
|
||||
end
|
||||
|
||||
# Create "thinking" indicator
|
||||
Turbo::StreamsChannel.broadcast_replace_to(
|
||||
chat,
|
||||
target: "thinking",
|
||||
html: '<div id="thinking" class="py-2 px-4"><div class="flex items-center"><div class="typing-indicator"></div></div></div>'
|
||||
)
|
||||
# Show initial thinking indicator - use replace instead of update to ensure it works for follow-up messages
|
||||
update_thinking_indicator(chat, "Thinking...")
|
||||
|
||||
# Create AI response
|
||||
ai_response = chat.messages.create!(
|
||||
role: "assistant",
|
||||
content: generate_response(chat, user_message.content)
|
||||
)
|
||||
# Processing steps with progress updates
|
||||
begin
|
||||
# Step 1: Preparing request
|
||||
update_thinking_indicator(chat, "Preparing request...")
|
||||
sleep(0.5) # Small delay to show the progress
|
||||
|
||||
# Broadcast the response to the chat channel
|
||||
Turbo::StreamsChannel.broadcast_append_to(
|
||||
chat,
|
||||
target: "messages",
|
||||
partial: "messages/message",
|
||||
locals: { message: ai_response }
|
||||
)
|
||||
# Step 2: Analyzing query
|
||||
update_thinking_indicator(chat, "Analyzing your question...")
|
||||
sleep(0.5) # Small delay to show the progress
|
||||
|
||||
# Hide the thinking indicator
|
||||
Turbo::StreamsChannel.broadcast_replace_to(
|
||||
chat,
|
||||
target: "thinking",
|
||||
html: '<div id="thinking" class="hidden"></div>'
|
||||
)
|
||||
# Step 3: Generating response
|
||||
update_thinking_indicator(chat, "Generating response...")
|
||||
|
||||
# Generate the actual response
|
||||
response_content = generate_response(chat, user_message.content)
|
||||
|
||||
# Step 4: Finalizing
|
||||
update_thinking_indicator(chat, "Finalizing response...")
|
||||
sleep(0.5) # Small delay to show the progress
|
||||
|
||||
# Create AI response
|
||||
ai_response = chat.messages.create!(
|
||||
role: "assistant",
|
||||
content: response_content
|
||||
)
|
||||
|
||||
# Broadcast the response to the chat channel
|
||||
Turbo::StreamsChannel.broadcast_append_to(
|
||||
chat,
|
||||
target: "messages",
|
||||
partial: "messages/message",
|
||||
locals: { message: ai_response }
|
||||
)
|
||||
rescue => e
|
||||
Rails.logger.error("Error in ProcessAiResponseJob: #{e.message}")
|
||||
Rails.logger.error(e.backtrace.join("\n"))
|
||||
|
||||
# Create an error message if something went wrong
|
||||
error_message = chat.messages.create!(
|
||||
role: "assistant",
|
||||
content: "I'm sorry, I encountered an error while processing your request. Please try again later."
|
||||
)
|
||||
|
||||
# Broadcast the error message
|
||||
Turbo::StreamsChannel.broadcast_append_to(
|
||||
chat,
|
||||
target: "messages",
|
||||
partial: "messages/message",
|
||||
locals: { message: error_message }
|
||||
)
|
||||
ensure
|
||||
# Hide the thinking indicator - use replace instead of update
|
||||
Turbo::StreamsChannel.broadcast_replace_to(
|
||||
chat,
|
||||
target: "thinking",
|
||||
html: '<div id="thinking" class="hidden"></div>'
|
||||
)
|
||||
|
||||
# Reset the form
|
||||
Turbo::StreamsChannel.broadcast_replace_to(
|
||||
chat,
|
||||
target: "message_form",
|
||||
partial: "messages/form",
|
||||
locals: { chat: chat, message: Message.new, scroll_behavior: true }
|
||||
)
|
||||
end
|
||||
|
||||
# Debug mode: Log completion
|
||||
Ai::DebugMode.log_to_chat(chat, "🐞 DEBUG: Processing completed")
|
||||
end
|
||||
|
||||
private
|
||||
# Helper method to update the thinking indicator with progress
|
||||
def update_thinking_indicator(chat, message)
|
||||
Turbo::StreamsChannel.broadcast_replace_to(
|
||||
chat,
|
||||
target: "thinking",
|
||||
html: <<~HTML
|
||||
<div id="thinking" class="flex items-start gap-3">
|
||||
#{ApplicationController.render(partial: "layouts/shared/ai_avatar")}
|
||||
<div class="bg-gray-100 rounded-lg p-4 max-w-[85%] flex items-center">
|
||||
<div class="flex gap-1">
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.4s"></div>
|
||||
</div>
|
||||
<span class="ml-2 text-gray-600">#{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
HTML
|
||||
)
|
||||
end
|
||||
|
||||
def generate_response(chat, user_message)
|
||||
# Use our financial assistant for responses
|
||||
|
|
|
@ -45,7 +45,8 @@ module Ai
|
|||
model: "gpt-4o",
|
||||
messages: messages,
|
||||
tools: financial_function_definitions.map { |func| { type: "function", function: func } },
|
||||
tool_choice: "auto"
|
||||
tool_choice: "auto",
|
||||
temperature: 0.5
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -185,6 +186,7 @@ module Ai
|
|||
})
|
||||
end
|
||||
|
||||
# If there are no function calls, ensure the direct response is concise
|
||||
return message["content"] unless message["tool_calls"]
|
||||
|
||||
# Handle function calls
|
||||
|
@ -232,6 +234,12 @@ module Ai
|
|||
}
|
||||
end
|
||||
|
||||
# Add a reminder to be concise
|
||||
follow_up_messages << {
|
||||
role: "system",
|
||||
content: "CRITICAL: Eliminate all unnecessary words."
|
||||
}
|
||||
|
||||
# Log the follow-up request in debug mode
|
||||
if Ai::DebugMode.enabled? && @chat
|
||||
Ai::DebugMode.log_to_chat(@chat, "🐞 DEBUG: Follow-up request", { messages: follow_up_messages })
|
||||
|
@ -239,8 +247,9 @@ module Ai
|
|||
|
||||
follow_up_response = client.chat(
|
||||
parameters: {
|
||||
model: "gpt-4-turbo",
|
||||
messages: follow_up_messages
|
||||
model: "gpt-4o",
|
||||
messages: follow_up_messages,
|
||||
temperature: 0.5
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -545,36 +554,47 @@ module Ai
|
|||
You are a helpful financial assistant for Maybe, a personal finance app.
|
||||
You help users understand their financial data by answering questions about their accounts, transactions, income, expenses, and net worth.
|
||||
|
||||
When users ask financial questions, use the provided functions to retrieve the relevant data.
|
||||
Always provide thoughtful analysis of the data, not just raw numbers.
|
||||
When users ask financial questions:
|
||||
1. Use the provided functions to retrieve the relevant data
|
||||
2. Provide ONLY the most important numbers and insights
|
||||
3. Eliminate all unnecessary words and context
|
||||
4. Use simple markdown for formatting
|
||||
5. Ask follow-up questions to keep the conversation going. Help educate the user about their own data and entice them to ask more questions.
|
||||
|
||||
The user's financial data is available through various function calls, and you should use these functions
|
||||
to provide accurate, data-driven responses.
|
||||
DO NOT:
|
||||
- Add introductions or conclusions
|
||||
- Apologize or explain limitations
|
||||
|
||||
Present monetary values consistently using the format provided by the functions.
|
||||
Whenever possible, provide insights and trends rather than just raw data.
|
||||
|
||||
Format your responses using Markdown for better readability. Use:
|
||||
- **Bold** and *italic* for emphasis
|
||||
- Lists (- 1. *) for organized information
|
||||
- `code` for technical terms or values
|
||||
- Tables for structured data when appropriate
|
||||
|
||||
Don't add markdown headings to your responses.
|
||||
|
||||
Be conversational and friendly in your responses. This is a chat interface, so maintain context across messages.
|
||||
Respond directly to the user's questions and provide helpful suggestions when appropriate.
|
||||
|
||||
Encourage the user to ask specific questions about their finances, such as:
|
||||
- "What's my net worth?"
|
||||
- "How much did I spend on groceries last month?"
|
||||
- "How has my spending changed compared to last month?"
|
||||
- "What's my savings rate this year?"
|
||||
- "Which category did I spend the most on?"
|
||||
- "How are my investments performing?"
|
||||
|
||||
Remember that financial information is sensitive, so maintain a professional, private, and secure approach.
|
||||
Present monetary values using the format provided by the functions.
|
||||
PROMPT
|
||||
end
|
||||
|
||||
# Ensure the response is concise by truncating if necessary
|
||||
def ensure_concise_response(content)
|
||||
return "" if content.nil? || content.empty?
|
||||
|
||||
# Split the content into sentences
|
||||
sentences = content.split(/(?<=[.!?])\s+/)
|
||||
|
||||
# Handle case where regex doesn't match (e.g., single sentence without ending punctuation)
|
||||
sentences = [ content ] if sentences.empty?
|
||||
|
||||
# Take only the first 3 sentences maximum
|
||||
sentences = sentences[0..2]
|
||||
|
||||
# Join the sentences back together
|
||||
truncated = sentences.join(" ")
|
||||
|
||||
# Further limit by word count (75 words maximum)
|
||||
words = truncated.split(/\s+/)
|
||||
if words.length > 75
|
||||
truncated = words[0...75].join(" ")
|
||||
# Ensure the truncated content ends with proper punctuation
|
||||
truncated = truncated.strip
|
||||
truncated += "." unless truncated.end_with?(".", "!", "?")
|
||||
end
|
||||
|
||||
truncated
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
<div data-chat-menu-target="defaultContent" data-chat-scroll-target="messages" class="flex flex-col h-full">
|
||||
<% if Current.user && @chat.present? %>
|
||||
<%# Existing Chat: Show messages or greeting if empty %>
|
||||
<div id="messages" data-turbo-cache="false" class="flex-grow">
|
||||
<div id="messages" data-turbo-cache="false" class="flex-grow" data-controller="chat-progress">
|
||||
<% messages = @messages&.where(internal: [false, nil]) %>
|
||||
<% if messages.any? %>
|
||||
<% messages.each do |message| %>
|
||||
|
@ -113,25 +113,38 @@
|
|||
<%= render "layouts/shared/ai_greeting", context: 'chat' %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# Thinking indicator - moved outside of messages div to prevent it from being replaced %>
|
||||
<div id="thinking" class="hidden flex items-start gap-3 mb-4 w-full" data-chat-progress-target="thinking">
|
||||
<%= render "layouts/shared/ai_avatar" %>
|
||||
<div class="bg-gray-100 rounded-lg p-4 max-w-[85%] flex items-center text-gray-800">
|
||||
<div class="flex gap-1">
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.4s"></div>
|
||||
</div>
|
||||
<span class="ml-2 text-gray-600">Thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%# New Chat: Show greeting in the top section %>
|
||||
<div class="flex-grow">
|
||||
<div class="flex-grow" data-controller="chat-progress">
|
||||
<%= render "layouts/shared/ai_greeting", context: 'default' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Thinking indicator %>
|
||||
<div id="thinking" class="hidden flex items-start gap-1 w-full">
|
||||
<%= render "layouts/shared/ai_avatar" %>
|
||||
<div class="p-4 max-w-[85%] flex items-center text-gray-800">
|
||||
<div class="flex gap-1">
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.4s"></div>
|
||||
|
||||
<%# Thinking indicator - moved outside of messages div to prevent it from being replaced %>
|
||||
<div id="thinking" class="hidden flex items-start gap-3 mb-4 w-full" data-chat-progress-target="thinking">
|
||||
<%= render "layouts/shared/ai_avatar" %>
|
||||
<div class="bg-gray-100 rounded-lg p-4 max-w-[85%] flex items-center text-gray-800">
|
||||
<div class="flex gap-1">
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.4s"></div>
|
||||
</div>
|
||||
<span class="ml-2 text-gray-600">Thinking...</span>
|
||||
</div>
|
||||
<span class="ml-2 text-gray-600">Thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<% end %>
|
||||
|
||||
<% if @message.user? %>
|
||||
<%= turbo_stream.update "thinking" do %>
|
||||
<div id="thinking" class="flex items-start gap-3">
|
||||
<%= turbo_stream.replace "thinking" do %>
|
||||
<div id="thinking" class="flex items-start gap-3 mb-4">
|
||||
<%= render "layouts/shared/ai_avatar" %>
|
||||
<div class="bg-gray-100 rounded-lg p-4 max-w-[85%] flex items-center">
|
||||
<div class="flex gap-1">
|
||||
|
@ -16,4 +16,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_stream.replace "message_form" do %>
|
||||
<%= render "messages/form", chat: @chat, message: Message.new, scroll_behavior: true, form_options: { data: { controller: "message-form", action: "turbo:submit-end->chat-scroll#scrollToBottom" } } %>
|
||||
<% end %>
|
||||
<% end %>
|
2
db/schema.rb
generated
2
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_02_20_200735) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_03_04_140435) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue