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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +94 -0
  3. data/LICENSE +201 -0
  4. data/README.md +176 -0
  5. data/Rakefile +15 -0
  6. data/app/assets/tailwind/layered/assistant/styles.css +13 -0
  7. data/app/controllers/concerns/layered/assistant/message_creation.rb +49 -0
  8. data/app/controllers/concerns/layered/assistant/public/session_conversations.rb +50 -0
  9. data/app/controllers/layered/assistant/application_controller.rb +25 -0
  10. data/app/controllers/layered/assistant/assistants_controller.rb +59 -0
  11. data/app/controllers/layered/assistant/conversations_controller.rb +77 -0
  12. data/app/controllers/layered/assistant/messages_controller.rb +57 -0
  13. data/app/controllers/layered/assistant/models_controller.rb +61 -0
  14. data/app/controllers/layered/assistant/panel/conversations_controller.rb +63 -0
  15. data/app/controllers/layered/assistant/panel/messages_controller.rb +44 -0
  16. data/app/controllers/layered/assistant/providers_controller.rb +55 -0
  17. data/app/controllers/layered/assistant/public/application_controller.rb +16 -0
  18. data/app/controllers/layered/assistant/public/assistants_controller.rb +16 -0
  19. data/app/controllers/layered/assistant/public/conversations_controller.rb +33 -0
  20. data/app/controllers/layered/assistant/public/messages_controller.rb +42 -0
  21. data/app/controllers/layered/assistant/public/panel/conversations_controller.rb +62 -0
  22. data/app/controllers/layered/assistant/public/panel/messages_controller.rb +50 -0
  23. data/app/controllers/layered/assistant/setup_controller.rb +9 -0
  24. data/app/helpers/layered/assistant/access_helper.rb +45 -0
  25. data/app/helpers/layered/assistant/messages_helper.rb +41 -0
  26. data/app/helpers/layered/assistant/panel_helper.rb +38 -0
  27. data/app/javascript/layered_assistant/composer_controller.js +30 -0
  28. data/app/javascript/layered_assistant/index.js +14 -0
  29. data/app/javascript/layered_assistant/message_streaming.js +124 -0
  30. data/app/javascript/layered_assistant/messages_controller.js +62 -0
  31. data/app/javascript/layered_assistant/panel_controller.js +36 -0
  32. data/app/javascript/layered_assistant/panel_nav_controller.js +16 -0
  33. data/app/javascript/layered_assistant/provider_template_controller.js +45 -0
  34. data/app/javascript/layered_assistant/vendor/marked.esm.js +72 -0
  35. data/app/jobs/layered/assistant/application_job.rb +6 -0
  36. data/app/jobs/layered/assistant/messages/response_job.rb +36 -0
  37. data/app/models/layered/assistant/application_record.rb +7 -0
  38. data/app/models/layered/assistant/assistant.rb +22 -0
  39. data/app/models/layered/assistant/conversation.rb +39 -0
  40. data/app/models/layered/assistant/message.rb +56 -0
  41. data/app/models/layered/assistant/model.rb +21 -0
  42. data/app/models/layered/assistant/provider.rb +49 -0
  43. data/app/services/layered/assistant/chunk_service.rb +80 -0
  44. data/app/services/layered/assistant/client_service.rb +18 -0
  45. data/app/services/layered/assistant/clients/anthropic.rb +24 -0
  46. data/app/services/layered/assistant/clients/base.rb +29 -0
  47. data/app/services/layered/assistant/clients/openai.rb +33 -0
  48. data/app/services/layered/assistant/messages_service.rb +58 -0
  49. data/app/services/layered/assistant/models/create_service.rb +50 -0
  50. data/app/services/layered/assistant/token_estimator.rb +11 -0
  51. data/app/views/layered/assistant/assistants/_form.html.erb +42 -0
  52. data/app/views/layered/assistant/assistants/edit.html.erb +6 -0
  53. data/app/views/layered/assistant/assistants/index.html.erb +45 -0
  54. data/app/views/layered/assistant/assistants/new.html.erb +6 -0
  55. data/app/views/layered/assistant/conversations/_form.html.erb +29 -0
  56. data/app/views/layered/assistant/conversations/edit.html.erb +6 -0
  57. data/app/views/layered/assistant/conversations/index.html.erb +63 -0
  58. data/app/views/layered/assistant/conversations/new.html.erb +6 -0
  59. data/app/views/layered/assistant/conversations/show.html.erb +25 -0
  60. data/app/views/layered/assistant/messages/_composer.html.erb +15 -0
  61. data/app/views/layered/assistant/messages/_message.html.erb +25 -0
  62. data/app/views/layered/assistant/messages/_system_prompt.html.erb +10 -0
  63. data/app/views/layered/assistant/messages/create.turbo_stream.erb +9 -0
  64. data/app/views/layered/assistant/messages/index.html.erb +46 -0
  65. data/app/views/layered/assistant/models/_form.html.erb +30 -0
  66. data/app/views/layered/assistant/models/edit.html.erb +6 -0
  67. data/app/views/layered/assistant/models/index.html.erb +54 -0
  68. data/app/views/layered/assistant/models/new.html.erb +6 -0
  69. data/app/views/layered/assistant/panel/conversations/_header.html.erb +23 -0
  70. data/app/views/layered/assistant/panel/conversations/index.html.erb +46 -0
  71. data/app/views/layered/assistant/panel/conversations/new.html.erb +23 -0
  72. data/app/views/layered/assistant/panel/conversations/show.html.erb +24 -0
  73. data/app/views/layered/assistant/panel/messages/_composer.html.erb +15 -0
  74. data/app/views/layered/assistant/panel/messages/create.turbo_stream.erb +9 -0
  75. data/app/views/layered/assistant/providers/_form.html.erb +81 -0
  76. data/app/views/layered/assistant/providers/edit.html.erb +6 -0
  77. data/app/views/layered/assistant/providers/index.html.erb +47 -0
  78. data/app/views/layered/assistant/providers/new.html.erb +6 -0
  79. data/app/views/layered/assistant/public/assistants/index.html.erb +34 -0
  80. data/app/views/layered/assistant/public/assistants/show.html.erb +23 -0
  81. data/app/views/layered/assistant/public/conversations/show.html.erb +24 -0
  82. data/app/views/layered/assistant/public/messages/_composer.html.erb +7 -0
  83. data/app/views/layered/assistant/public/messages/create.turbo_stream.erb +9 -0
  84. data/app/views/layered/assistant/public/panel/conversations/_header.html.erb +16 -0
  85. data/app/views/layered/assistant/public/panel/conversations/index.html.erb +48 -0
  86. data/app/views/layered/assistant/public/panel/conversations/new.html.erb +17 -0
  87. data/app/views/layered/assistant/public/panel/conversations/show.html.erb +23 -0
  88. data/app/views/layered/assistant/public/panel/messages/_composer.html.erb +7 -0
  89. data/app/views/layered/assistant/public/panel/messages/create.turbo_stream.erb +9 -0
  90. data/app/views/layered/assistant/setup/_setup.html.erb +121 -0
  91. data/app/views/layered/assistant/setup/index.html.erb +2 -0
  92. data/app/views/layouts/layered/assistant/_host_navigation.html.erb +0 -0
  93. data/app/views/layouts/layered/assistant/application.html.erb +32 -0
  94. data/config/importmap.rb +8 -0
  95. data/config/routes.rb +31 -0
  96. data/data/models.json +42 -0
  97. data/db/migrate/20260312000000_create_layered_assistant_tables.rb +63 -0
  98. data/lib/generators/layered/assistant/install_generator.rb +113 -0
  99. data/lib/generators/layered/assistant/migrations_generator.rb +47 -0
  100. data/lib/generators/layered/assistant/templates/initializer.rb +26 -0
  101. data/lib/layered/assistant/engine.rb +29 -0
  102. data/lib/layered/assistant/version.rb +5 -0
  103. data/lib/layered/assistant.rb +19 -0
  104. data/lib/layered-assistant-rails.rb +1 -0
  105. 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&amp;.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>&lt;% if l_assistant_accessible? %&gt;
51
+ &lt;%= link_to "Assistant", layered_assistant.root_path %&gt;
52
+ &lt;% end %&gt;</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>&lt;% content_for :l_ui_panel_heading do %&gt;
60
+ &lt;%= layered_assistant_panel_header %&gt;
61
+ &lt;% end %&gt;
62
+
63
+ &lt;% content_for :l_ui_panel_body do %&gt;
64
+ &lt;%= layered_assistant_panel_body %&gt;
65
+ &lt;% end %&gt;
66
+
67
+ &lt;%= render template: "layouts/layered_ui/application" %&gt;</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>&lt;%= layered_assistant_panel_body data: { controller: "panel" } %&gt;</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>
@@ -0,0 +1,2 @@
1
+ <h1>Setup</h1>
2
+ <%= render "layered/assistant/setup/setup" %>
@@ -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" %>
@@ -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