layered-assistant-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/AGENTS.md +94 -0
- data/LICENSE +201 -0
- data/README.md +176 -0
- data/Rakefile +15 -0
- data/app/assets/tailwind/layered/assistant/styles.css +13 -0
- data/app/controllers/concerns/layered/assistant/message_creation.rb +49 -0
- data/app/controllers/concerns/layered/assistant/public/session_conversations.rb +50 -0
- data/app/controllers/layered/assistant/application_controller.rb +25 -0
- data/app/controllers/layered/assistant/assistants_controller.rb +59 -0
- data/app/controllers/layered/assistant/conversations_controller.rb +77 -0
- data/app/controllers/layered/assistant/messages_controller.rb +57 -0
- data/app/controllers/layered/assistant/models_controller.rb +61 -0
- data/app/controllers/layered/assistant/panel/conversations_controller.rb +63 -0
- data/app/controllers/layered/assistant/panel/messages_controller.rb +44 -0
- data/app/controllers/layered/assistant/providers_controller.rb +55 -0
- data/app/controllers/layered/assistant/public/application_controller.rb +16 -0
- data/app/controllers/layered/assistant/public/assistants_controller.rb +16 -0
- data/app/controllers/layered/assistant/public/conversations_controller.rb +33 -0
- data/app/controllers/layered/assistant/public/messages_controller.rb +42 -0
- data/app/controllers/layered/assistant/public/panel/conversations_controller.rb +62 -0
- data/app/controllers/layered/assistant/public/panel/messages_controller.rb +50 -0
- data/app/controllers/layered/assistant/setup_controller.rb +9 -0
- data/app/helpers/layered/assistant/access_helper.rb +45 -0
- data/app/helpers/layered/assistant/messages_helper.rb +41 -0
- data/app/helpers/layered/assistant/panel_helper.rb +38 -0
- data/app/javascript/layered_assistant/composer_controller.js +30 -0
- data/app/javascript/layered_assistant/index.js +14 -0
- data/app/javascript/layered_assistant/message_streaming.js +124 -0
- data/app/javascript/layered_assistant/messages_controller.js +62 -0
- data/app/javascript/layered_assistant/panel_controller.js +36 -0
- data/app/javascript/layered_assistant/panel_nav_controller.js +16 -0
- data/app/javascript/layered_assistant/provider_template_controller.js +45 -0
- data/app/javascript/layered_assistant/vendor/marked.esm.js +72 -0
- data/app/jobs/layered/assistant/application_job.rb +6 -0
- data/app/jobs/layered/assistant/messages/response_job.rb +36 -0
- data/app/models/layered/assistant/application_record.rb +7 -0
- data/app/models/layered/assistant/assistant.rb +22 -0
- data/app/models/layered/assistant/conversation.rb +39 -0
- data/app/models/layered/assistant/message.rb +56 -0
- data/app/models/layered/assistant/model.rb +21 -0
- data/app/models/layered/assistant/provider.rb +49 -0
- data/app/services/layered/assistant/chunk_service.rb +80 -0
- data/app/services/layered/assistant/client_service.rb +18 -0
- data/app/services/layered/assistant/clients/anthropic.rb +24 -0
- data/app/services/layered/assistant/clients/base.rb +29 -0
- data/app/services/layered/assistant/clients/openai.rb +33 -0
- data/app/services/layered/assistant/messages_service.rb +58 -0
- data/app/services/layered/assistant/models/create_service.rb +50 -0
- data/app/services/layered/assistant/token_estimator.rb +11 -0
- data/app/views/layered/assistant/assistants/_form.html.erb +42 -0
- data/app/views/layered/assistant/assistants/edit.html.erb +6 -0
- data/app/views/layered/assistant/assistants/index.html.erb +45 -0
- data/app/views/layered/assistant/assistants/new.html.erb +6 -0
- data/app/views/layered/assistant/conversations/_form.html.erb +29 -0
- data/app/views/layered/assistant/conversations/edit.html.erb +6 -0
- data/app/views/layered/assistant/conversations/index.html.erb +63 -0
- data/app/views/layered/assistant/conversations/new.html.erb +6 -0
- data/app/views/layered/assistant/conversations/show.html.erb +25 -0
- data/app/views/layered/assistant/messages/_composer.html.erb +15 -0
- data/app/views/layered/assistant/messages/_message.html.erb +25 -0
- data/app/views/layered/assistant/messages/_system_prompt.html.erb +10 -0
- data/app/views/layered/assistant/messages/create.turbo_stream.erb +9 -0
- data/app/views/layered/assistant/messages/index.html.erb +46 -0
- data/app/views/layered/assistant/models/_form.html.erb +30 -0
- data/app/views/layered/assistant/models/edit.html.erb +6 -0
- data/app/views/layered/assistant/models/index.html.erb +54 -0
- data/app/views/layered/assistant/models/new.html.erb +6 -0
- data/app/views/layered/assistant/panel/conversations/_header.html.erb +23 -0
- data/app/views/layered/assistant/panel/conversations/index.html.erb +46 -0
- data/app/views/layered/assistant/panel/conversations/new.html.erb +23 -0
- data/app/views/layered/assistant/panel/conversations/show.html.erb +24 -0
- data/app/views/layered/assistant/panel/messages/_composer.html.erb +15 -0
- data/app/views/layered/assistant/panel/messages/create.turbo_stream.erb +9 -0
- data/app/views/layered/assistant/providers/_form.html.erb +81 -0
- data/app/views/layered/assistant/providers/edit.html.erb +6 -0
- data/app/views/layered/assistant/providers/index.html.erb +47 -0
- data/app/views/layered/assistant/providers/new.html.erb +6 -0
- data/app/views/layered/assistant/public/assistants/index.html.erb +34 -0
- data/app/views/layered/assistant/public/assistants/show.html.erb +23 -0
- data/app/views/layered/assistant/public/conversations/show.html.erb +24 -0
- data/app/views/layered/assistant/public/messages/_composer.html.erb +7 -0
- data/app/views/layered/assistant/public/messages/create.turbo_stream.erb +9 -0
- data/app/views/layered/assistant/public/panel/conversations/_header.html.erb +16 -0
- data/app/views/layered/assistant/public/panel/conversations/index.html.erb +48 -0
- data/app/views/layered/assistant/public/panel/conversations/new.html.erb +17 -0
- data/app/views/layered/assistant/public/panel/conversations/show.html.erb +23 -0
- data/app/views/layered/assistant/public/panel/messages/_composer.html.erb +7 -0
- data/app/views/layered/assistant/public/panel/messages/create.turbo_stream.erb +9 -0
- data/app/views/layered/assistant/setup/_setup.html.erb +121 -0
- data/app/views/layered/assistant/setup/index.html.erb +2 -0
- data/app/views/layouts/layered/assistant/_host_navigation.html.erb +0 -0
- data/app/views/layouts/layered/assistant/application.html.erb +32 -0
- data/config/importmap.rb +8 -0
- data/config/routes.rb +31 -0
- data/data/models.json +42 -0
- data/db/migrate/20260312000000_create_layered_assistant_tables.rb +63 -0
- data/lib/generators/layered/assistant/install_generator.rb +113 -0
- data/lib/generators/layered/assistant/migrations_generator.rb +47 -0
- data/lib/generators/layered/assistant/templates/initializer.rb +26 -0
- data/lib/layered/assistant/engine.rb +29 -0
- data/lib/layered/assistant/version.rb +5 -0
- data/lib/layered/assistant.rb +19 -0
- data/lib/layered-assistant-rails.rb +1 -0
- metadata +449 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<h1>Assistants</h1>
|
|
2
|
+
|
|
3
|
+
<% if @assistants.any? %>
|
|
4
|
+
<div class="l-ui-container--table l-ui-utility--mt-lg">
|
|
5
|
+
<table class="l-ui-table">
|
|
6
|
+
<caption class="l-ui-sr-only">Public assistants</caption>
|
|
7
|
+
<thead class="l-ui-table__header">
|
|
8
|
+
<tr>
|
|
9
|
+
<th scope="col" class="l-ui-table__header-cell">Name</th>
|
|
10
|
+
<th scope="col" class="l-ui-table__header-cell">Description</th>
|
|
11
|
+
</tr>
|
|
12
|
+
</thead>
|
|
13
|
+
|
|
14
|
+
<tbody class="l-ui-table__body">
|
|
15
|
+
<% @assistants.each do |assistant| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<th scope="row" class="l-ui-table__cell--primary">
|
|
18
|
+
<%= link_to assistant.name, layered_assistant.public_assistant_path(assistant) %>
|
|
19
|
+
</th>
|
|
20
|
+
<td class="l-ui-table__cell">
|
|
21
|
+
<%= truncate(assistant.description, length: 60) %>
|
|
22
|
+
</td>
|
|
23
|
+
</tr>
|
|
24
|
+
<% end %>
|
|
25
|
+
</tbody>
|
|
26
|
+
</table>
|
|
27
|
+
|
|
28
|
+
<div class="l-ui-utility--mt-lg">
|
|
29
|
+
<%= l_ui_pagy(@pagy) %>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<% else %>
|
|
33
|
+
<p>No assistants are currently available.</p>
|
|
34
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<div class="l-ui-container--spread">
|
|
2
|
+
<h1><%= @assistant.name %></h1>
|
|
3
|
+
<%= link_to "Back", layered_assistant.public_assistants_path, class: "l-ui-button--outline" %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<dl class="l-ui-description-list l-ui-utility--mt-lg">
|
|
7
|
+
<% if @assistant.description.present? %>
|
|
8
|
+
<dt>Description</dt>
|
|
9
|
+
<dd><%= @assistant.description %></dd>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<% if @assistant.default_model.present? %>
|
|
13
|
+
<dt>Default model</dt>
|
|
14
|
+
<dd><%= @assistant.default_model.name %></dd>
|
|
15
|
+
<% end %>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
<div class="l-ui-utility--mt-lg">
|
|
19
|
+
<%= form_with url: layered_assistant.public_conversations_path, class: "l-ui-form" do |f| %>
|
|
20
|
+
<%= f.hidden_field :assistant_id, value: @assistant.id %>
|
|
21
|
+
<%= f.submit "New conversation", class: "l-ui-button--primary" %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<%= turbo_stream_from @conversation %>
|
|
2
|
+
|
|
3
|
+
<div class="l-ui-conversation__container">
|
|
4
|
+
<div class="l-ui-container--spread">
|
|
5
|
+
<h1><%= @conversation.name %></h1>
|
|
6
|
+
<%= link_to "Back", layered_assistant.public_assistant_path(@conversation.assistant), class: "l-ui-button--outline" %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="l-ui-conversation__messages" data-controller="messages">
|
|
10
|
+
<div id="<%= dom_id(@conversation) %>_messages" class="<%= dom_id(@conversation) %>_messages l-ui-conversation" aria-live="polite" data-messages-target="list">
|
|
11
|
+
<%= render partial: "layered/assistant/messages/message", collection: @messages, as: :message %>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<button class="l-ui-scroll-to-bottom"
|
|
15
|
+
data-messages-target="scrollButton"
|
|
16
|
+
data-action="click->messages#jumpToBottom"
|
|
17
|
+
aria-label="Scroll to bottom"
|
|
18
|
+
type="button">
|
|
19
|
+
<%= image_tag "layered_ui/icon_chevron_down.svg", class: "l-ui-icon--sm l-ui-icon--dark-invert", alt: "" %>
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<%= render "layered/assistant/public/messages/composer", conversation: @conversation %>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= form_with url: layered_assistant.public_conversation_messages_path(conversation), id: "public-composer-form", data: { controller: "composer", composer_target: "form", turbo_frame: "_top", action: "submit->composer#submit" } do |f| %>
|
|
2
|
+
<div class="l-ui-conversation__composer">
|
|
3
|
+
<%= f.text_area :content, name: "message[content]", class: "l-ui-conversation__composer-input", placeholder: "Type a message...", "aria-label": "Message", rows: 1, data: { composer_target: "input", action: "keydown->composer#submitOnEnter" } %>
|
|
4
|
+
|
|
5
|
+
<button type="submit" class="l-ui-button--primary" title="Send (Enter)" data-composer-target="button">Send</button>
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% if @error %>
|
|
2
|
+
<%= turbo_stream.append_all ".#{dom_id(@conversation)}_messages" do %>
|
|
3
|
+
<div class="l-ui-notice--error l-ui-utility--mb-0" role="alert"><%= @error %></div>
|
|
4
|
+
<% end %>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<%= turbo_stream.replace "public-composer-form" do %>
|
|
8
|
+
<%= render "layered/assistant/public/messages/composer", conversation: @conversation %>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%= turbo_frame_tag "assistant_panel_header" do %>
|
|
2
|
+
<div class="l-ui-form__group l-ui-utility--mt-0">
|
|
3
|
+
<div class="l-ui-select-wrapper l-ui-utility--mt-0 l-ui-utility---mr-2">
|
|
4
|
+
<select name="conversation_id" class="l-ui-select" aria-label="Select conversation"
|
|
5
|
+
data-controller="panel-nav"
|
|
6
|
+
data-action="change->panel-nav#navigate"
|
|
7
|
+
data-panel-nav-new-url-value="<%= layered_assistant.new_public_panel_conversation_path(assistant_id: assistant.id) %>">
|
|
8
|
+
<option value="<%= layered_assistant.public_panel_conversations_path(assistant_id: assistant.id) %>" <%= "selected" if selected == :index %>>Conversations</option>
|
|
9
|
+
<option value="new" <%= "selected" if selected == :new %>>+ New</option>
|
|
10
|
+
<% conversations.each do |c| %>
|
|
11
|
+
<option value="<%= layered_assistant.public_panel_conversation_path(c) %>" <%= "selected" if selected == c.id %>><%= c.name %></option>
|
|
12
|
+
<% end %>
|
|
13
|
+
</select>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<%= render "layered/assistant/public/panel/conversations/header", assistant: @assistant, conversations: @conversations, selected: :index %>
|
|
2
|
+
|
|
3
|
+
<%= turbo_frame_tag "assistant_panel" do %>
|
|
4
|
+
<div class="l-assistant-panel__lobby">
|
|
5
|
+
<h2 class="l-ui-heading--md"><%= @assistant.name %></h2>
|
|
6
|
+
|
|
7
|
+
<% if @assistant.description.present? %>
|
|
8
|
+
<p class="l-ui-text"><%= @assistant.description %></p>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<% if @conversations.any? %>
|
|
12
|
+
<div class="l-ui-container--table">
|
|
13
|
+
<table class="l-ui-table l-ui-utility--mt-0">
|
|
14
|
+
<caption class="l-ui-sr-only">Your conversations</caption>
|
|
15
|
+
<thead class="l-ui-table__header">
|
|
16
|
+
<tr>
|
|
17
|
+
<th scope="col" class="l-ui-table__header-cell">Created</th>
|
|
18
|
+
<th scope="col" class="l-ui-table__header-cell">Name</th>
|
|
19
|
+
<th scope="col" class="l-ui-table__header-cell">Messages</th>
|
|
20
|
+
</tr>
|
|
21
|
+
</thead>
|
|
22
|
+
|
|
23
|
+
<tbody class="l-ui-table__body">
|
|
24
|
+
<% @conversations.each do |conversation| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td class="l-ui-table__cell">
|
|
27
|
+
<%= conversation.created_at.to_fs(:short) %>
|
|
28
|
+
</td>
|
|
29
|
+
<th scope="row" class="l-ui-table__cell--primary">
|
|
30
|
+
<%= link_to conversation.name, layered_assistant.public_panel_conversation_path(conversation), data: { turbo_frame: "assistant_panel" } %>
|
|
31
|
+
</th>
|
|
32
|
+
<td class="l-ui-table__cell">
|
|
33
|
+
<%= conversation.messages_count || 0 %>
|
|
34
|
+
</td>
|
|
35
|
+
</tr>
|
|
36
|
+
<% end %>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<%= form_with url: layered_assistant.public_panel_conversations_path(assistant_id: @assistant.id), method: :post do |f| %>
|
|
43
|
+
<div class="l-ui-utility--mt-lg">
|
|
44
|
+
<%= f.submit "Start conversation", class: "l-ui-button--primary" %>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%= render "layered/assistant/public/panel/conversations/header", assistant: @assistant, conversations: [], selected: :new %>
|
|
2
|
+
|
|
3
|
+
<%= turbo_frame_tag "assistant_panel" do %>
|
|
4
|
+
<div class="l-assistant-panel__lobby">
|
|
5
|
+
<h2 class="l-ui-heading--md"><%= @assistant.name %></h2>
|
|
6
|
+
|
|
7
|
+
<% if @assistant.description.present? %>
|
|
8
|
+
<p class="l-ui-text"><%= @assistant.description %></p>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<%= form_with url: layered_assistant.public_panel_conversations_path(assistant_id: @assistant.id), method: :post do |f| %>
|
|
12
|
+
<div class="l-ui-utility--mt-lg">
|
|
13
|
+
<%= f.submit "Start conversation", class: "l-ui-button--primary" %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%= render "layered/assistant/public/panel/conversations/header", assistant: @conversation.assistant, conversations: @conversations, selected: @conversation.id %>
|
|
2
|
+
|
|
3
|
+
<%= turbo_frame_tag "assistant_panel" do %>
|
|
4
|
+
<div class="l-ui-conversation__container">
|
|
5
|
+
<%= turbo_stream_from @conversation %>
|
|
6
|
+
|
|
7
|
+
<div class="l-ui-conversation__messages" data-controller="messages">
|
|
8
|
+
<div id="public_panel_<%= dom_id(@conversation) %>_messages" class="<%= dom_id(@conversation) %>_messages l-ui-conversation" aria-live="polite" data-messages-target="list">
|
|
9
|
+
<%= render partial: "layered/assistant/messages/message", collection: @messages, as: :message %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<button class="l-ui-scroll-to-bottom"
|
|
13
|
+
data-messages-target="scrollButton"
|
|
14
|
+
data-action="click->messages#jumpToBottom"
|
|
15
|
+
aria-label="Scroll to bottom"
|
|
16
|
+
type="button">
|
|
17
|
+
<%= image_tag "layered_ui/icon_chevron_down.svg", class: "l-ui-icon--sm l-ui-icon--dark-invert", alt: "" %>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<%= render "layered/assistant/public/panel/messages/composer", conversation: @conversation %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= form_with url: layered_assistant.public_panel_conversation_messages_path(conversation), id: "public-panel-composer-form", data: { controller: "composer", composer_target: "form", turbo_frame: "_top", action: "submit->composer#submit" } do |f| %>
|
|
2
|
+
<div class="l-ui-conversation__composer">
|
|
3
|
+
<%= f.text_area :content, name: "message[content]", class: "l-ui-conversation__composer-input", placeholder: "Type a message...", "aria-label": "Message", rows: 1, data: { composer_target: "input", action: "keydown->composer#submitOnEnter" } %>
|
|
4
|
+
|
|
5
|
+
<button type="submit" class="l-ui-button--primary" title="Send (Enter)" data-composer-target="button">Send</button>
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% if @error %>
|
|
2
|
+
<%= turbo_stream.append_all ".#{dom_id(@conversation)}_messages" do %>
|
|
3
|
+
<div class="l-ui-notice--error l-ui-utility--mb-0" role="alert"><%= @error %></div>
|
|
4
|
+
<% end %>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<%= turbo_stream.replace "public-panel-composer-form" do %>
|
|
8
|
+
<%= render "layered/assistant/public/panel/messages/composer", conversation: @conversation %>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<p class="l-ui-utility--mt-md">Run the install generator to copy CSS, JS, and migrations to your application:</p>
|
|
2
|
+
<code class="l-ui-surface l-ui-utility--mt-md">bin/rails generate layered:assistant:install</code>
|
|
3
|
+
<p class="l-ui-utility--mt-lg">This will:</p>
|
|
4
|
+
<ul class="l-ui-list l-ui-utility--mt-sm">
|
|
5
|
+
<li>Copy <code>layered_ui.css</code> to <code>app/assets/tailwind/</code></li>
|
|
6
|
+
<li>Add <code>@import "./layered_ui";</code> to your <code>application.css</code></li>
|
|
7
|
+
<li>Add <code>import "layered_ui"</code> to your <code>application.js</code></li>
|
|
8
|
+
<li>Copy <code>layered_assistant.css</code> to <code>app/assets/tailwind/layered_assistant.css</code></li>
|
|
9
|
+
<li>Add <code>@import "./layered_assistant";</code> to your <code>application.css</code> (after the layered-ui import)</li>
|
|
10
|
+
<li>Add <code>import "layered_assistant"</code> to your <code>application.js</code> (after the layered-ui import)</li>
|
|
11
|
+
<li>Mount the engine at <code>/layered/assistant</code> in your <code>config/routes.rb</code></li>
|
|
12
|
+
<li>Copy engine migrations into your application</li>
|
|
13
|
+
</ul>
|
|
14
|
+
<p class="l-ui-utility--mt-md">All steps are idempotent - re-running the generator will not duplicate imports, routes, or migrations.</p>
|
|
15
|
+
|
|
16
|
+
<h2 class="l-ui-utility--mt-xl">Authorization</h2>
|
|
17
|
+
<p class="l-ui-utility--mt-md">
|
|
18
|
+
All non-public engine routes are blocked by default (403 Forbidden) until you configure an
|
|
19
|
+
<code>authorize</code> block in <code>config/initializers/layered_assistant.rb</code>.
|
|
20
|
+
</p>
|
|
21
|
+
<p class="l-ui-utility--mt-md">
|
|
22
|
+
The install generator creates this file for you. Uncomment one of the examples to get started.
|
|
23
|
+
</p>
|
|
24
|
+
<p class="l-ui-utility--mt-lg">
|
|
25
|
+
The block runs in controller context, so you have access to
|
|
26
|
+
<code>request</code>, <code>current_user</code>, <code>redirect_to</code>,
|
|
27
|
+
<code>head</code>, <code>main_app</code>, and all other controller methods.
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<h3 class="l-ui-utility--mt-lg">Allow all requests</h3>
|
|
31
|
+
<pre class="l-ui-surface l-ui-utility--mt-md"><code>Layered::Assistant.authorize do
|
|
32
|
+
# No-op: all requests permitted
|
|
33
|
+
end</code></pre>
|
|
34
|
+
|
|
35
|
+
<h3 class="l-ui-utility--mt-lg">Require sign-in (Devise)</h3>
|
|
36
|
+
<pre class="l-ui-surface l-ui-utility--mt-md"><code>Layered::Assistant.authorize do
|
|
37
|
+
redirect_to main_app.new_user_session_path unless user_signed_in?
|
|
38
|
+
end</code></pre>
|
|
39
|
+
|
|
40
|
+
<h3 class="l-ui-utility--mt-lg">Restrict to admins</h3>
|
|
41
|
+
<pre class="l-ui-surface l-ui-utility--mt-md"><code>Layered::Assistant.authorize do
|
|
42
|
+
head :forbidden unless current_user&.admin?
|
|
43
|
+
end</code></pre>
|
|
44
|
+
|
|
45
|
+
<h3 class="l-ui-utility--mt-lg">Checking access in views</h3>
|
|
46
|
+
<p class="l-ui-utility--mt-md">
|
|
47
|
+
The <code>l_assistant_accessible?</code> helper evaluates the authorize block
|
|
48
|
+
without side effects. Use it to conditionally show navigation or links to the engine:
|
|
49
|
+
</p>
|
|
50
|
+
<pre class="l-ui-surface l-ui-utility--mt-md"><code><% if l_assistant_accessible? %>
|
|
51
|
+
<%= link_to "Assistant", layered_assistant.root_path %>
|
|
52
|
+
<% end %></code></pre>
|
|
53
|
+
|
|
54
|
+
<h2 class="l-ui-utility--mt-xl">Panel helpers</h2>
|
|
55
|
+
<p class="l-ui-utility--mt-md">
|
|
56
|
+
Two helpers are available to wire the <code>layered-ui</code> panel to the assistant engine.
|
|
57
|
+
Add these to your application layout's <code>content_for</code> blocks:
|
|
58
|
+
</p>
|
|
59
|
+
<pre class="l-ui-surface l-ui-utility--mt-md"><code><% content_for :l_ui_panel_heading do %>
|
|
60
|
+
<%= layered_assistant_panel_header %>
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
<% content_for :l_ui_panel_body do %>
|
|
64
|
+
<%= layered_assistant_panel_body %>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
67
|
+
<%= render template: "layouts/layered_ui/application" %></code></pre>
|
|
68
|
+
<p class="l-ui-utility--mt-lg">Both helpers accept keyword arguments forwarded as HTML attributes:</p>
|
|
69
|
+
<pre class="l-ui-surface l-ui-utility--mt-md"><code><%= layered_assistant_panel_body data: { controller: "panel" } %></code></pre>
|
|
70
|
+
|
|
71
|
+
<div class="l-ui-container--table l-ui-utility--mt-lg">
|
|
72
|
+
<table class="l-ui-table">
|
|
73
|
+
<caption class="l-ui-sr-only">Panel helpers reference</caption>
|
|
74
|
+
<thead class="l-ui-table__header">
|
|
75
|
+
<tr>
|
|
76
|
+
<th class="l-ui-table__header-cell" scope="col">Helper</th>
|
|
77
|
+
<th class="l-ui-table__header-cell" scope="col">Description</th>
|
|
78
|
+
</tr>
|
|
79
|
+
</thead>
|
|
80
|
+
<tbody class="l-ui-table__body">
|
|
81
|
+
<tr>
|
|
82
|
+
<th class="l-ui-table__cell--primary" scope="row"><code>layered_assistant_panel_header</code></th>
|
|
83
|
+
<td class="l-ui-table__cell">Empty Turbo Frame populated by the engine's panel views</td>
|
|
84
|
+
</tr>
|
|
85
|
+
<tr>
|
|
86
|
+
<th class="l-ui-table__cell--primary" scope="row"><code>layered_assistant_panel_body</code></th>
|
|
87
|
+
<td class="l-ui-table__cell">Turbo Frame that loads the conversation list from the engine's panel routes</td>
|
|
88
|
+
</tr>
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<h2 class="l-ui-utility--mt-xl">Getting started</h2>
|
|
94
|
+
<ol class="l-ui-list l-ui-utility--mt-md">
|
|
95
|
+
<li><%= link_to "Create a provider", layered_assistant.new_provider_path %> (e.g. Anthropic or OpenAI) with your API key</li>
|
|
96
|
+
<li>Add one or more models to the provider</li>
|
|
97
|
+
<li><%= link_to "Create an assistant", layered_assistant.new_assistant_path %> and assign a default model</li>
|
|
98
|
+
<li><%= link_to "Start a conversation", layered_assistant.new_conversation_path %></li>
|
|
99
|
+
</ol>
|
|
100
|
+
|
|
101
|
+
<h2 class="l-ui-utility--mt-xl">License</h2>
|
|
102
|
+
<p class="l-ui-utility--mt-md">
|
|
103
|
+
Released under the <a href="https://github.com/layered-ai-public/layered-assistant-rails/blob/main/LICENSE">Apache 2.0 License</a>.
|
|
104
|
+
</p>
|
|
105
|
+
<p class="l-ui-utility--mt-md">
|
|
106
|
+
Copyright 2026 LAYERED AI LIMITED (UK company number: 17056830).
|
|
107
|
+
See <a href="https://github.com/layered-ai-public/layered-assistant-rails/blob/main/NOTICE">NOTICE</a> for attribution details.
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
<h2 class="l-ui-utility--mt-xl">Trademarks</h2>
|
|
111
|
+
<p class="l-ui-utility--mt-md">
|
|
112
|
+
The source code is fully open, but the layered.ai name, logo, and brand assets are trademarks of LAYERED AI LIMITED.
|
|
113
|
+
The Apache 2.0 license does not grant rights to use the layered.ai branding.
|
|
114
|
+
Forks and redistributions must use a distinct name.
|
|
115
|
+
See <a href="https://github.com/layered-ai-public/layered-assistant-rails/blob/main/TRADEMARK.md">TRADEMARK.md</a> for the full policy.
|
|
116
|
+
</p>
|
|
117
|
+
|
|
118
|
+
<h2 class="l-ui-utility--mt-xl">Contributing</h2>
|
|
119
|
+
<ul class="l-ui-list l-ui-utility--mt-md">
|
|
120
|
+
<li><a href="https://github.com/layered-ai-public/layered-assistant-rails/blob/main/CLA.md">CLA.md</a> - contributor licence agreement</li>
|
|
121
|
+
</ul>
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<% content_for :l_ui_navigation_items do %>
|
|
2
|
+
<%= render "layouts/layered/assistant/host_navigation" %>
|
|
3
|
+
<% if l_assistant_accessible? %>
|
|
4
|
+
<%= l_ui_navigation_item "Setup", layered_assistant.root_path %>
|
|
5
|
+
<%= l_ui_navigation_item "Providers", layered_assistant.providers_path %>
|
|
6
|
+
<%= l_ui_navigation_item "Assistants", layered_assistant.assistants_path %>
|
|
7
|
+
<%= l_ui_navigation_item "Conversations", layered_assistant.conversations_path %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<% content_for :l_ui_panel_heading do %>
|
|
12
|
+
<% if l_assistant_accessible? %>
|
|
13
|
+
<%= layered_assistant_panel_header %>
|
|
14
|
+
<% else %>
|
|
15
|
+
<%= layered_assistant_panel_header do %>Assistant<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% if l_assistant_accessible? %>
|
|
20
|
+
<% content_for :l_ui_panel_body do %>
|
|
21
|
+
<%= layered_assistant_panel_body data: { controller: "panel" } %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% else %>
|
|
24
|
+
<% public_assistant = Layered::Assistant::Assistant.publicly_available.by_name.first %>
|
|
25
|
+
<% if public_assistant %>
|
|
26
|
+
<% content_for :l_ui_panel_body do %>
|
|
27
|
+
<%= layered_assistant_public_panel_body assistant: public_assistant, data: { controller: "panel", panel_storage_key_value: "public_assistant_panel_url" } %>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<%= render template: "layouts/layered_ui/application" %>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pin "layered_assistant", to: "layered_assistant/index.js"
|
|
2
|
+
pin "layered_assistant/message_streaming", to: "layered_assistant/message_streaming.js"
|
|
3
|
+
pin "layered_assistant/composer_controller", to: "layered_assistant/composer_controller.js"
|
|
4
|
+
pin "layered_assistant/messages_controller", to: "layered_assistant/messages_controller.js"
|
|
5
|
+
pin "layered_assistant/panel_controller", to: "layered_assistant/panel_controller.js"
|
|
6
|
+
pin "layered_assistant/panel_nav_controller", to: "layered_assistant/panel_nav_controller.js"
|
|
7
|
+
pin "layered_assistant/provider_template_controller", to: "layered_assistant/provider_template_controller.js"
|
|
8
|
+
pin "marked", to: "layered_assistant/vendor/marked.esm.js"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Layered::Assistant::Engine.routes.draw do
|
|
2
|
+
root "setup#index"
|
|
3
|
+
resources :assistants, except: [:show] do
|
|
4
|
+
resources :conversations, only: [:index]
|
|
5
|
+
end
|
|
6
|
+
resources :providers, only: [:index, :new, :create, :edit, :update, :destroy] do
|
|
7
|
+
resources :models, only: [:index, :new, :create, :edit, :update, :destroy]
|
|
8
|
+
end
|
|
9
|
+
resources :conversations, only: [:index, :show, :new, :create, :edit, :update, :destroy] do
|
|
10
|
+
resources :messages, only: [:index, :create, :destroy]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
namespace :panel do
|
|
14
|
+
resources :conversations, only: [:index, :show, :new, :create, :destroy] do
|
|
15
|
+
resources :messages, only: [:create]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
namespace :public do
|
|
20
|
+
resources :assistants, only: [:index, :show]
|
|
21
|
+
resources :conversations, only: [:show, :create] do
|
|
22
|
+
resources :messages, only: [:create]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
namespace :panel do
|
|
26
|
+
resources :conversations, only: [:index, :show, :new, :create] do
|
|
27
|
+
resources :messages, only: [:create]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/data/models.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Anthropic": [
|
|
3
|
+
{ "name": "Claude Opus 4.6", "identifier": "claude-opus-4-6" },
|
|
4
|
+
{ "name": "Claude Sonnet 4.6", "identifier": "claude-sonnet-4-6" },
|
|
5
|
+
{ "name": "Claude Haiku 4.5", "identifier": "claude-haiku-4-5" }
|
|
6
|
+
],
|
|
7
|
+
"OpenAI": [
|
|
8
|
+
{ "name": "GPT-5.4", "identifier": "gpt-5.4" },
|
|
9
|
+
{ "name": "GPT-5 Mini", "identifier": "gpt-5-mini" }
|
|
10
|
+
],
|
|
11
|
+
"Gemini": [
|
|
12
|
+
{ "name": "Gemini 3.1 Pro Preview", "identifier": "gemini-3.1-pro-preview" },
|
|
13
|
+
{ "name": "Gemini 2.5 Flash Lite", "identifier": "gemini-2.5-flash-lite" },
|
|
14
|
+
{ "name": "Gemini 2.5 Flash", "identifier": "gemini-2.5-flash" },
|
|
15
|
+
{ "name": "Gemini 2.5 Pro", "identifier": "gemini-2.5-pro" }
|
|
16
|
+
],
|
|
17
|
+
"OpenRouter": [
|
|
18
|
+
{ "name": "MiniMax M2.5", "identifier": "minimax/minimax-m2.5" },
|
|
19
|
+
{ "name": "DeepSeek V3.2", "identifier": "deepseek/deepseek-v3.2" },
|
|
20
|
+
{ "name": "Grok 4", "identifier": "x-ai/grok-4" },
|
|
21
|
+
{ "name": "Grok 4 (Fast)", "identifier": "x-ai/grok-4-fast" },
|
|
22
|
+
{ "name": "Grok 3", "identifier": "x-ai/grok-3" },
|
|
23
|
+
{ "name": "Llama 4 Maverick", "identifier": "meta-llama/llama-4-maverick-17b-128e-instruct" },
|
|
24
|
+
{ "name": "Llama 4 Scout", "identifier": "meta-llama/llama-4-scout-17b-16e-instruct" }
|
|
25
|
+
],
|
|
26
|
+
"Groq": [
|
|
27
|
+
{ "name": "Compound", "identifier": "groq/compound" },
|
|
28
|
+
{ "name": "Compound Mini", "identifier": "groq/compound-mini" },
|
|
29
|
+
{ "name": "GPT-OSS 120B", "identifier": "openai/gpt-oss-120b" },
|
|
30
|
+
{ "name": "Llama 4 Maverick", "identifier": "meta-llama/llama-4-maverick-17b-128e-instruct" },
|
|
31
|
+
{ "name": "Llama 4 Scout", "identifier": "meta-llama/llama-4-scout-17b-16e-instruct" },
|
|
32
|
+
{ "name": "Llama 3.3 70B Versatile", "identifier": "llama-3.3-70b-versatile" },
|
|
33
|
+
{ "name": "Llama 3.1 8B Instant", "identifier": "llama-3.1-8b-instant" },
|
|
34
|
+
{ "name": "Qwen 3 32B", "identifier": "qwen/qwen3-32b" },
|
|
35
|
+
{ "name": "Kimi K2.5", "identifier": "moonshotai/kimi-k2-instruct-0905" }
|
|
36
|
+
],
|
|
37
|
+
"Mistral": [
|
|
38
|
+
{ "name": "Mistral Large 2512", "identifier": "mistral-large-2512" },
|
|
39
|
+
{ "name": "Mistral Medium 2508", "identifier": "mistral-medium-2508" },
|
|
40
|
+
{ "name": "Mistral Small 2506", "identifier": "mistral-small-2506" }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
class CreateLayeredAssistantTables < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :layered_assistant_providers, if_not_exists: true do |t|
|
|
4
|
+
t.references :owner, polymorphic: true
|
|
5
|
+
t.string :protocol, null: false
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :url
|
|
8
|
+
t.string :secret
|
|
9
|
+
t.boolean :enabled, default: true, null: false
|
|
10
|
+
t.integer :position, null: false
|
|
11
|
+
t.integer :models_count, default: 0, null: false
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
create_table :layered_assistant_models, if_not_exists: true do |t|
|
|
16
|
+
t.references :provider, null: false, foreign_key: { to_table: :layered_assistant_providers }
|
|
17
|
+
t.string :identifier, null: false
|
|
18
|
+
t.string :name, null: false
|
|
19
|
+
t.boolean :enabled, default: true, null: false
|
|
20
|
+
t.integer :position, null: false
|
|
21
|
+
t.bigint :assistants_count, default: 0, null: false
|
|
22
|
+
t.bigint :messages_count, default: 0, null: false
|
|
23
|
+
t.timestamps
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
create_table :layered_assistant_assistants, if_not_exists: true do |t|
|
|
27
|
+
t.string :uid, null: false, index: { unique: true }
|
|
28
|
+
t.references :owner, polymorphic: true
|
|
29
|
+
t.references :default_model, foreign_key: { to_table: :layered_assistant_models }
|
|
30
|
+
t.string :name, null: false
|
|
31
|
+
t.text :description
|
|
32
|
+
t.text :system_prompt
|
|
33
|
+
t.boolean :public, default: false, null: false
|
|
34
|
+
t.bigint :conversations_count, default: 0, null: false
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
create_table :layered_assistant_conversations, if_not_exists: true do |t|
|
|
39
|
+
t.string :uid, null: false, index: { unique: true }
|
|
40
|
+
t.references :owner, polymorphic: true
|
|
41
|
+
t.references :subject, polymorphic: true
|
|
42
|
+
t.references :assistant, null: false, foreign_key: { to_table: :layered_assistant_assistants }
|
|
43
|
+
t.string :name, null: false
|
|
44
|
+
t.bigint :input_tokens
|
|
45
|
+
t.bigint :output_tokens
|
|
46
|
+
t.bigint :token_estimate
|
|
47
|
+
t.bigint :messages_count, default: 0, null: false
|
|
48
|
+
t.timestamps
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
create_table :layered_assistant_messages, if_not_exists: true do |t|
|
|
52
|
+
t.string :uid, null: false, index: { unique: true }
|
|
53
|
+
t.references :conversation, null: false, foreign_key: { to_table: :layered_assistant_conversations }
|
|
54
|
+
t.references :model, foreign_key: { to_table: :layered_assistant_models }
|
|
55
|
+
t.string :role, null: false, default: "system"
|
|
56
|
+
t.text :content
|
|
57
|
+
t.bigint :input_tokens
|
|
58
|
+
t.bigint :output_tokens
|
|
59
|
+
t.boolean :tokens_estimated, default: false, null: false
|
|
60
|
+
t.timestamps
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|