I am having issue using turbo frame and and turbo stream:
There is a detailed version of the implementations and the logs.
Current setup
Ruby 3.2.0
Rails 8.0.1
# config/routes.rb
Rails.application.routes.draw do
mount MissionControl::Jobs::Engine, at: "/jobs"
get "up" => "rails/health#show", as: :rails_health_check
resources :messages
root "messages#index"
end
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def index
@messages = Message.all.order(created_at: :asc)
end
def create
@user_message = Message.create(
content: params[:content],
role: "user"
)
@assistant_message = Message.create(
content: "Searching the answer for you...",
role: "assistant"
)
ProcessChatMessageJob.perform_later(@user_message, @assistant_message)
respond_to(&:turbo_stream)
end
end
class Message < ApplicationRecord
validates :content, presence: true, length: { minimum: 3, maximum: 1000 }
after_create_commit do
broadcast_append_to "messages", target: "messages", partial: "messages/message", locals: { message: self }
end
after_update_commit do
broadcast_update_to "messages", target: "message_#{id}", partial: "messages/message", locals: { message: self }
end
end
<!-- app/views/messages/index.html.erb -->
<div class="max-w-2xl mx-auto p-4 space-y-6">
<div id="messages" class="border p-4 rounded shadow">
<%= turbo_stream_from "messages" %>
<% @messages.each do |message| %>
<%= render partial: "messages/message", locals: { message: message } %>
<% end %>
</div>
<div id="message_form">
<%= render "messages/form" %>
</div>
</div>
<!-- app/views/messages/create.turbo_stream.erb -->
<%= turbo_stream.replace "message_form" do %>
<%= render "messages/form" %>
<% end %>
<!-- app/views/messages/_form.html.erb -->
<%= form_with url: messages_path, method: :post, data: { turbo_frame: "messages" }, class: "flex flex-col gap-2" do |f| %>
<%= f.text_area :content, placeholder: "Enter a new message...", class: "p-2 border rounded", rows: 3 %>
<%= f.submit "Send", class: "px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" %>
<% end %>
<!-- app/views/messages/_message.html.erb -->
<%= turbo_frame_tag dom_id(message) do %>
<div class="p-4">
<div class="flex my-1 <%= message.role == 'user' ? 'justify-end' : 'justify-start' %>">
<div class="<%= message.role == 'user' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800' %> max-w-xs md:max-w-md p-4 rounded-lg shadow-lg">
<%= markdown(message.content) %>
</div>
</div>
</div>
<% end %>
# app/jobs/process_chat_message_job.rb
class ProcessChatMessageJob < ApplicationJob
queue_as :default
def perform(user_message, assistant_message)
user_message_content = user_message.content
sql_query = Ai::SqlQueryGenerator.new(user_message_content).call
if sql_query.include?("Sorry")
assistant_message.update!(content: sql_query)
return
end
puts "Executing SQL query: #{sql_query}"
result = execute_sql_query(sql_query)
puts "Result of sql query: #{result}"
formatted_answer = Ai::ResultFormatter.new(result).call
puts "Formatted answer: #{formatted_answer}"
assistant_message.update!(content: formatted_answer)
end
private
def execute_sql_query(sql_query)
ActiveRecord::Base.connection.execute(sql_query)
rescue => e
puts "Error executing SQL query: #{e.message}"
nil
end
end
Logs (Excerpt)
# Job Enqueue & Execution
[ActiveJob] Enqueued ProcessChatMessageJob (...) with arguments: Message/406, Message/407
...
[ActiveJob] Performing ProcessChatMessageJob (...)
Executing SQL query: SELECT ... FROM tournament_years ty ... LIMIT 20;
...
Message Update (...) UPDATE "messages" SET "content" = '# Grand Slam Wins by Legendary Tennis Players ...' WHERE "messages"."id" = 407
...
# Broadcasting Updated Message
[ActiveJob] Enqueued Turbo::Streams::ActionBroadcastJob ... with arguments: "messages", { action: :replace, target: "message_407", partial: "messages/message", locals: { message: Message/407 } }
...
[ActiveJob] Performed Turbo::Streams::ActionBroadcastJob (Job ID: ...) from SolidQueue(default) in ...ms
Detailed Explanation of the Issue
Current Behavior
• On Form Submission:
The MessagesController#create action creates two records:
-
A user message (e.g. “give me top 20 players that won the most grand slam ?”).
-
An assistant message with the placeholder text “Searching the answer for you…”.
Both records trigger after_create_commit callbacks which broadcast an append event to the “messages” stream. In your index view, you subscribe to this stream and render all messages inside a container with id=“messages”.
Additionally, the view has a separate container (id=“message_form”) for the form, which is replaced by a turbo stream (from create.turbo_stream.erb) after submission.
• After ActiveJob Processing:
The background job (ProcessChatMessageJob) receives both the user message and the assistant message. It then:
• Calls your SQL query generator and formatter.
• Updates the assistant message with the formatted answer.
• The after_update_commit callback on Message triggers a broadcast update (using broadcast_replace_later_to) targeting the turbo frame with dom_id(message).
In logs, you see an append broadcast (for creation) targeting “messages” and later a replace broadcast targeting “message_407” (for example).
Problems Observed
- AI Response Not Updating:
After the background job finishes processing the API response,
The assistant message is well updated inside the database.
It seems that it’s broadcasted to the stream but still the message is not being updated on the page.
My understanding of hotwire and turbo is limited and I don’t seems to see where something is going wrong.
From the logs i feel like all the messages are broadcasted rightfully
I don’t know where to look for anymore.
Anyone would have any ideas of what’s wrong about the implementation ?
Thanks,