Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assistants page #559

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ gem "turbo-rails", "~> 2.0.5"
gem "stimulus-rails", "~> 1.3.3"
gem "tailwindcss-rails", "~> 2.7.2"
gem "rack-cors"
gem "ostruct"

# Build JSON APIs with ease [https://github.com/rails/jbuilder]
# gem "jbuilder"
Expand Down
9 changes: 9 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ def redirect_to(*args)

private

def set_nav_conversations
@nav_conversations = Conversation.grouped_by_increasing_time_interval_for_user(Current.user)
end

def set_nav_assistants
@nav_assistants = Current.user.assistants.ordered
end


def set_system_ivars
@system_ivars = public_ivars
end
Expand Down
10 changes: 8 additions & 2 deletions app/controllers/assistants_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
class AssistantsController < ApplicationController
before_action :set_nav_conversations
before_action :set_nav_assistants

def index
assistant = Current.user.assistants.ordered.first
redirect_to new_assistant_message_path(assistant), status: :see_other
unless Feature.assistants_page?
best_assistant = @nav_assistants.first
redirect_to new_assistant_message_path(best_assistant), status: :see_other
return
end
end
end
8 changes: 0 additions & 8 deletions app/controllers/conversations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,6 @@ def destroy

private

def set_nav_conversations
@nav_conversations = Conversation.grouped_by_increasing_time_interval_for_user(Current.user)
end

def set_nav_assistants
@nav_assistants = Current.user.assistants.ordered
end

def set_conversation
@conversation = Current.user.conversations.find(params[:id])
end
Expand Down
12 changes: 9 additions & 3 deletions app/models/user/registerable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ def create_initial_assistants_etc

LanguageModel.import_from_file(users: [self])

assistants.create! name: "GPT-4o", language_model: language_models.best_for_api_service(open_ai_api_service).first
assistants.create! name: "Claude 3.5 Sonnet", language_model: language_models.best_for_api_service(anthropic_api_service).first
assistants.create! name: "Meta Llama", language_model: language_models.best_for_api_service(groq_api_service).first
[
["GPT-4o", open_ai_api_service],
["Claude 3.5 Sonnet", anthropic_api_service],
["Meta Llama 3 70b", groq_api_service],
].map do |name, api_service|
language_model = language_models.best_for_api_service(api_service).first
description = "Model #{language_model.api_name} on #{api_service.name}"
assistants.create! name:, description:, language_model:
end
end
end
17 changes: 9 additions & 8 deletions app/views/assistants/_assistant.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<%# locals: (assistant:, settings: true, assistant_counter: -1) %>
<% selected = assistant == @assistant %>
<% first = assistant_counter.zero? %>
<% visible = assistant_counter <= Assistant::MAX_LIST_DISPLAY-1 || selected %>
<div class="bg-gray-50 dark:bg-gray-900 <%= first && 'absolute pt-[17px] left-0 pl-3 w-full z-10 top-0' %>">
<%# This extra div ^ is needed because of the absolute positioning. It doesn't lay out properly if added to the div below. %>
<div class="<%= class_names(
"bg-gray-50 dark:bg-gray-900",
"first:sticky first:top-0 first:pt-2 first:z-40 first:bg-gray-50 dark:first:bg-gray-900": !Feature.assistants_page?
) %>">
<div class="
flex justify-between items-center
mb-1 pl-2 pr-2 mr-5
Expand All @@ -18,11 +19,11 @@
data-role="assistant"
data-radio-behavior-target="radio"
data-action="radio-changed@window->radio-behavior#select"
data-radio-behavior-id-param="<%= assistant.id %>"
data-transition-target="<%= !visible && 'transitionable' %>"
data-radio-behavior-id-param="<%= assistant.id %>"
data-transition-target="<%= !visible && 'transitionable' %>"
>
<%= link_to new_assistant_message_path(assistant), class: "flex-1 flex py-1 items-center text-gray-950 dark:text-gray-100 font-medium truncate", data: { role: "name" } do %>
<%= render partial: "layouts/assistant_avatar", locals: { assistant: assistant, size: 7, classes: "mr-2" } %>
<%= render partial: "layouts/assistant_avatar", locals: { assistant:, size: 7, classes: "mr-2" } %>
<%= assistant.name %>
<% end %>
<div class="hidden gap-3 pl-2 relationship:flex group-hover:flex">
Expand All @@ -34,7 +35,7 @@
variant: :micro,
size: 18,
class: "outline-none text-gray-950 dark:text-gray-100 invisible group-hover:visible cursor-pointer",
'data-controller': "nested-pointer",
data: { controller: "nested-pointer" },
title: "More"
%>
<menu tabindex="0" class="dropdown-content z-10 menu p-2 shadow-xl bg-base-100 rounded-box w-52 -mr-10 mt-7 dark:!bg-gray-700" data-controller="nested-pointer">
Expand All @@ -49,7 +50,7 @@
size: 18,
class: "text-gray-950 dark:text-gray-100 cursor-pointer group-hover:visible invisible
relationship:visible relationship:text-gray-950 dark:relationship:text-gray-100",
'data-controller': "nested-pointer",
data: { controller: "nested-pointer" },
title: "New"
%>
<% end %>
Expand Down
12 changes: 12 additions & 0 deletions app/views/assistants/_assistant_index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="relative flex items-start space-x-3 rounded-lg border border-gray-300 bg-white px-6 py-5 shadow-sm focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:border-gray-400">
<div class="w-7">
<%= render partial: "layouts/assistant_avatar", locals: { assistant:, size: 7, classes: "border-none" } %>
</div>
<div class="min-w-0 flex-1">
<%= link_to new_assistant_message_path(assistant), data: { role: "name" } do %>
<span class="absolute inset-0" aria-hidden="true"></span>
<p class="text-sm font-medium text-gray-900"><%= assistant.name %></p>
<p class="text-sm text-gray-500"><%= assistant.description %></p>
<% end %>
</div>
</div>
11 changes: 11 additions & 0 deletions app/views/assistants/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%= content_for :nav_column do %>
<%= render 'messages/nav_column' %>
<% end %>

<div class="container mx-[2rem] my-[4rem]">
<h1 class="text-2xl mb-4 font-semibold tracking-tighter">Assistants</h1>

<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<%= render partial: "assistant_index", collection: @nav_assistants, as: :assistant %>
</div>
</div>
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
px-safe
"
>
<div id="nav-container" class="flex-1 flex flex-col h-full">
<div id="nav-container" class="flex-1 flex flex-col fixed z-20 inset-y-0">
<div id="nav-scrollable" class="flex-grow pl-3 overflow-y-auto overflow-x-clip scrollbar-hide overscroll-contain">
<%= yield :nav_column %>
</div>
Expand Down
18 changes: 17 additions & 1 deletion app/views/messages/_nav_column.html.erb
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
<% if Feature.assistants_page? %>
<div class="sticky top-0 pt-2 z-40 bg-gray-50 dark:bg-gray-900">
<%= link_to assistants_path, class: %|
flex
mb-1 p-1 pl-2 pr-2 mr-5
rounded-lg
text-sm
hover:bg-gray-100 dark:hover:bg-gray-700
| do %>
<%= icon "sparkles", variant: :outline %>
<span class="ml-2 font-semibold">Assistants</span>
<% end %>
</div>
<% end %>

<section
id="assistants"
data-controller="radio-behavior transition"
data-radio-behavior-selected-class="relationship !flex"
data-transition-toggle-class="hidden"
class="pt-14 flex flex-col"
class="flex flex-col"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed you removed this pt-14 and you within the _assistant_sidebar partial, you removed the first hack:

<%= first && 'absolute pt-[17px] left-0 pl-3 w-full z-10 top-0' %>

I remember this being some tricky css to get the sidebar to make the first assistant sticky as the other items scrolled underneath it. Were you able to solve it a different way?

Copy link
Contributor Author

@drnic drnic Nov 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I think this idea was getting in my way (since I wanted an "Assistants" header as the first thing) so I dropped it and assumed I'd come back to it later. I'll investigate this.

>

<%= render @nav_assistants %>

<%= button_tag type: "button",
Expand Down
1 change: 1 addition & 0 deletions config/options.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ shared:
default_to_voice: <%= ENV["DEFAULT_TO_VOICE_FEATURE"] || false %>
email: <%= ENV["EMAIL_FEATURE"] || default_to(false, except_env_test: true) %>
password_reset_email: <%= ENV["PASSWORD_RESET_EMAIL_FEATURE"] || default_to(false, except_env_test: true) %>
assistants_page: <%= ENV["ASSISTANTS_PAGE_FEATURE"] || false %>
settings:
# Be sure to add these ENV to docker-compose.yml
app_url_protocol: <%= ENV["APP_URL_PROTOCOL"] || default_to(app_url: :protocol) %>
Expand Down
13 changes: 11 additions & 2 deletions test/controllers/assistants_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ class AssistantsControllerTest < ActionDispatch::IntegrationTest
login_as @user
end

test "should get index" do
get assistants_url
test "index redirects to conversation if assistants_page is disabled" do
stub_features(assistants_page: false) do
get assistants_url
end
assert_redirected_to new_assistant_message_path(@assistant)
end

test "index shows assistants if assistants_page is enabled" do
stub_features(assistants_page: true) do
get assistants_url
end
assert_response :success
end
end
14 changes: 9 additions & 5 deletions test/controllers/concerns/authenticate/by_http_header_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

class Authenticate::ByHttpHeaderTest < ActionDispatch::IntegrationTest
test "should login user via header" do
stub_features(http_header_authentication: true) do
stub_features(http_header_authentication: true, assistants_page: false) do
get root_url, headers: existing_http_auth_user
assert_login_completed_for users(:rob)
end
end

test "should create and login new user" do
stub_features(http_header_authentication: true, registration: true) do
stub_features(http_header_authentication: true, registration: true, assistants_page: false) do
assert_difference "User.count", 1 do
assert_difference "Person.count", 1 do
get root_url, headers: new_http_auth_user
Expand All @@ -28,7 +28,7 @@ class Authenticate::ByHttpHeaderTest < ActionDispatch::IntegrationTest
end

test "should create and login new user when NAME IS OMITTED" do
stub_features(http_header_authentication: true, registration: true) do
stub_features(http_header_authentication: true, registration: true, assistants_page: false) do
assert_difference "User.count", 1 do
assert_difference "Person.count", 1 do
get root_url, headers: new_http_auth_user.except(Setting.http_header_auth_name)
Expand All @@ -53,6 +53,7 @@ class Authenticate::ByHttpHeaderTest < ActionDispatch::IntegrationTest
test "should render unauthorized if no HTTP header present and no other auth is allowed" do
stub_features(
http_header_authentication: true,
assistants_page: false,
password_authentication: false,
google_authentication: false,
microsoft_graph_authentication: false
Expand All @@ -66,6 +67,7 @@ class Authenticate::ByHttpHeaderTest < ActionDispatch::IntegrationTest
test "should render UN-AUTHORIZED if REGISTRATION DISABLED and NO HEADERS are provided and MANUAL AUTH IS DISABLED" do
stub_features(
http_header_authentication: true, # note: this disables manual auth (e.g. password, google)
assistants_page: false,
) do
get root_url
end
Expand All @@ -79,7 +81,8 @@ class Authenticate::ByHttpHeaderTest < ActionDispatch::IntegrationTest

stub_features(
http_header_authentication: true, # note: this disables manual auth (e.g. password, google)
registration: false
registration: false,
assistants_page: false,
) do
get root_url, headers: headers
end
Expand All @@ -93,7 +96,8 @@ class Authenticate::ByHttpHeaderTest < ActionDispatch::IntegrationTest

stub_features(
http_header_authentication: true,
registration: false
registration: false,
assistants_page: false
) do
get root_url, headers: headers
end
Expand Down
50 changes: 35 additions & 15 deletions test/controllers/users_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
end

test "should create user" do
post users_url, params: { person: { personable_type: "User", email: "[email protected]", personable_attributes: user_attr } }
assert_response :redirect
follow_redirect!
follow_redirect! # intentionally two redirects
assert_response :success
stub_features(assistants_page: true) do
post users_url, params: { person: { personable_type: "User", email: "[email protected]", personable_attributes: user_attr } }
assert_response :redirect
follow_redirect!
assert_response :success
end
end

test "should redirect back when the email address is already in use" do
Expand All @@ -51,19 +52,38 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_match "Email can&#39;t be blank", response.body
end

test "after create, an account should be bootstrapped and taken to a conversation" do
email = "fake_email#{rand(1000)}@example.com"
post users_url, params: { person: { personable_type: "User", email: email, personable_attributes: user_attr } }
test "after create, an account should be bootstrapped and redirect to new conversation if assistants_page is disabled" do
stub_features(assistants_page: false) do
email = "fake_email#{rand(1000)}@example.com"
post users_url, params: { person: { personable_type: "User", email: email, personable_attributes: user_attr } }

user = Person.find_by(email: email).user
assert_equal "John", user.first_name
assert_equal "Doe", user.last_name
assert_equal 3, user.assistants.count, "This new user did not get the expected number of assistants"

user = Person.find_by(email: email).user
assert_equal "John", user.first_name
assert_equal "Doe", user.last_name
assert_equal 3, user.assistants.count, "This new user did not get the expected number of assistants"
assert_redirected_to root_path
follow_redirect!
assistant = user.assistants.ordered.first
assert_redirected_to new_assistant_message_path(assistant)
end
end

test "after create, an account should be bootstrapped and shown assistants if assistants_page is enabled" do
stub_features(assistants_page: true) do
email = "fake_email#{rand(1000)}@example.com"
post users_url, params: { person: { personable_type: "User", email: email, personable_attributes: user_attr } }

assistant = user.assistants.ordered.first
user = Person.find_by(email: email).user
assert_equal "John", user.first_name
assert_equal "Doe", user.last_name
assert_equal 3, user.assistants.count, "This new user did not get the expected number of assistants"

follow_redirect!
assert_redirected_to new_assistant_message_path(assistant)
assert_redirected_to root_path
follow_redirect!
assert_response :success
assert_select "h1", "Assistants"
end
end

test "updates user preferences" do
Expand Down
20 changes: 11 additions & 9 deletions test/models/language_model/export_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,28 +76,30 @@ class LanguageModel::ExportTest < ActiveSupport::TestCase
end

test "import_from_file with existing models by api_name" do
model = users(:rob).language_models.not_deleted.first
user = users(:rob)
api_name = "gpt-4o"
model = user.language_models.find_by(api_name:)
models = [{
api_name: model.api_name,
name: "new name",
api_name:,
name: "new model name",
supports_images: false,
supports_tools: true,
input_token_cost_cents: 0.0001,
output_token_cost_cents: 0.0001
input_token_cost_cents: 0.1234,
output_token_cost_cents: 0.5678
}]
storage = {
"models" => models
}
path = Rails.root.join("tmp/newmodels.yml")
File.write(path, storage.to_yaml)
assert_no_difference "LanguageModel.count" do
LanguageModel.import_from_file(path:, users: users(:rob))
LanguageModel.import_from_file(path:, users: [user])
end
model.reload
assert_equal "new name", model.name
assert_equal "new model name", model.name
assert_equal false, model.supports_images
assert_equal true, model.supports_tools
assert_equal 0.0001, model.input_token_cost_cents
assert_equal 0.0001, model.output_token_cost_cents
assert_equal 0.1234, model.input_token_cost_cents
assert_equal 0.5678, model.output_token_cost_cents
end
end
4 changes: 3 additions & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def login_as(user_or_person, password = "secret")
post login_path, params: { email: user.email, password: password }
assert_response :redirect
follow_redirect! # root
follow_redirect! # conversation
unless Feature.assistants_page?
follow_redirect! # conversation
end
assert_response :success
end

Expand Down
Loading