Skip to content

Commit 4076001

Browse files
committed
Add chat UI scaffold generator with Turbo streaming
Creates a complete chat interface with: - Chat and message controllers following Rails conventions - Simple HTML views for chat list, creation, and messaging - Model selector in new chat form - Models index page showing available AI models - Background job for streaming AI responses - Turbo Stream integration for real-time message updates - Automatic broadcasting from Message model - Clean, simple controller methods The generator creates a working chat UI that can be customized while maintaining Rails best practices and simplicity.
1 parent 6c7d7be commit 4076001

19 files changed

Lines changed: 335 additions & 1 deletion

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ AllCops:
1010
- docs/**/*
1111
- vendor/**/*
1212
- gemfiles/**/*
13+
- lib/generators/**/templates/**/*
1314
SuggestExtensions: false
1415

1516
Metrics/ClassLength:
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails/generators'
4+
5+
module RubyLLM
6+
module Generators
7+
# Generates a simple chat UI scaffold for RubyLLM
8+
class ChatUIGenerator < Rails::Generators::Base
9+
source_root File.expand_path('templates', __dir__)
10+
11+
namespace 'ruby_llm:chat_ui'
12+
13+
def create_views
14+
# Chat views
15+
template 'views/chats/index.html.erb', 'app/views/chats/index.html.erb'
16+
template 'views/chats/new.html.erb', 'app/views/chats/new.html.erb'
17+
template 'views/chats/show.html.erb', 'app/views/chats/show.html.erb'
18+
template 'views/chats/_chat.html.erb', 'app/views/chats/_chat.html.erb'
19+
template 'views/chats/_form.html.erb', 'app/views/chats/_form.html.erb'
20+
21+
# Message views
22+
template 'views/messages/_message.html.erb', 'app/views/messages/_message.html.erb'
23+
template 'views/messages/_form.html.erb', 'app/views/messages/_form.html.erb'
24+
template 'views/messages/create.turbo_stream.erb', 'app/views/messages/create.turbo_stream.erb'
25+
26+
# Model views
27+
template 'views/models/index.html.erb', 'app/views/models/index.html.erb'
28+
template 'views/models/show.html.erb', 'app/views/models/show.html.erb'
29+
end
30+
31+
def create_controllers
32+
template 'controllers/chats_controller.rb', 'app/controllers/chats_controller.rb'
33+
template 'controllers/messages_controller.rb', 'app/controllers/messages_controller.rb'
34+
template 'controllers/models_controller.rb', 'app/controllers/models_controller.rb'
35+
end
36+
37+
def create_jobs
38+
template 'jobs/chat_response_job.rb', 'app/jobs/chat_response_job.rb'
39+
end
40+
41+
def add_routes
42+
route 'resources :models, only: [:index, :show]'
43+
route "resources :chats do\n resources :messages, only: [:create]\n end"
44+
end
45+
46+
def display_post_install_message
47+
readme 'README' if behavior == :invoke
48+
end
49+
end
50+
end
51+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
===============================================================================
2+
3+
The chat UI has been successfully generated!
4+
5+
Next steps:
6+
7+
1. Make sure your models are set up with the RubyLLM acts_as methods:
8+
9+
class Chat < ApplicationRecord
10+
acts_as_chat
11+
end
12+
13+
class Message < ApplicationRecord
14+
acts_as_message
15+
end
16+
17+
2. Run migrations if you haven't already:
18+
19+
rails db:migrate
20+
21+
3. Start your Rails server and visit:
22+
23+
http://localhost:3000/chats
24+
25+
4. The UI provides:
26+
- A list of all chats at /chats
27+
- Create new chats with an initial prompt at /chats/new
28+
- View and continue conversations at /chats/:id
29+
- Simple, clean HTML interface
30+
31+
===============================================================================
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class ChatsController < ApplicationController
2+
before_action :set_chat, only: [:show]
3+
4+
def index
5+
@chats = Chat.order(created_at: :desc)
6+
end
7+
8+
def new
9+
@chat = Chat.new
10+
end
11+
12+
def create
13+
return unless prompt.present?
14+
15+
@chat = Chat.create!(model: model)
16+
ChatResponseJob.perform_later(@chat.id, prompt)
17+
18+
redirect_to @chat, notice: 'Chat was successfully created.'
19+
end
20+
21+
def show
22+
@message = @chat.messages.build
23+
end
24+
25+
private
26+
27+
def set_chat
28+
@chat = Chat.find(params[:id])
29+
end
30+
31+
def model
32+
params[:chat][:model].presence
33+
end
34+
35+
def prompt
36+
params[:chat][:prompt]
37+
end
38+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
class MessagesController < ApplicationController
2+
before_action :set_chat
3+
4+
def create
5+
return unless content.present?
6+
7+
ChatResponseJob.perform_later(@chat.id, content)
8+
9+
respond_to do |format|
10+
format.turbo_stream
11+
format.html { redirect_to @chat }
12+
end
13+
end
14+
15+
private
16+
17+
def set_chat
18+
@chat = Chat.find(params[:chat_id])
19+
end
20+
21+
def content
22+
params[:message][:content]
23+
end
24+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class ModelsController < ApplicationController
2+
def index
3+
@models = Model.all.group_by(&:provider)
4+
end
5+
6+
def show
7+
@model = Model.find(params[:id])
8+
end
9+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class ChatResponseJob < ApplicationJob
2+
def perform(chat_id, content)
3+
chat = Chat.find(chat_id)
4+
5+
chat.ask(content) do |chunk|
6+
if chunk.content && !chunk.content.blank?
7+
message = chat.messages.last
8+
message.update!(content: message.content + chunk.content)
9+
end
10+
end
11+
end
12+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div id="<%%= dom_id chat %>">
2+
<p><strong>Chat #<%%= chat.id %></strong></p>
3+
<p><%%= chat.model.name %></p>
4+
<p><%%= chat.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
5+
<p><%%= pluralize(chat.messages.count, 'message') %></p>
6+
</div>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<%%= form_with(model: chat, url: chats_path) do |form| %>
2+
<%% if chat.errors.any? %>
3+
<div style="color: red">
4+
<h2><%%= pluralize(chat.errors.count, "error") %> prohibited this chat from being saved:</h2>
5+
6+
<ul>
7+
<%% chat.errors.each do |error| %>
8+
<li><%%= error.full_message %></li>
9+
<%% end %>
10+
</ul>
11+
</div>
12+
<%% end %>
13+
14+
<div>
15+
<%%= form.label :model, "Select AI model:", style: "display: block" %>
16+
<%%= form.select :model,
17+
options_for_select(Model.pluck(:name, :model_id).unshift(["Default (#{RubyLLM.config.default_model})", nil])),
18+
{},
19+
style: "width: 100%; max-width: 600px; padding: 5px;" %>
20+
</div>
21+
22+
<div style="margin-top: 15px;">
23+
<%%= form.label :prompt, "Enter your prompt:", style: "display: block" %>
24+
<%%= form.text_area :prompt, rows: 4, style: "width: 100%; max-width: 600px;" %>
25+
</div>
26+
27+
<div style="margin-top: 15px;">
28+
<%%= form.submit "Start new chat" %>
29+
</div>
30+
<%% end %>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<p style="color: green"><%%= notice %></p>
2+
3+
<%% content_for :title, "Chats" %>
4+
5+
<h1>Chats</h1>
6+
7+
<div id="chats">
8+
<%% @chats.each do |chat| %>
9+
<%%= render chat %>
10+
<p>
11+
<%%= link_to "Show this chat", chat %>
12+
</p>
13+
<%% end %>
14+
</div>
15+
16+
<%%= link_to "New chat", new_chat_path %>

0 commit comments

Comments
 (0)