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,63 @@
|
|
|
1
|
+
<div class="l-ui-container--spread">
|
|
2
|
+
<h1><%= @assistant ? "Conversations - #{@assistant.name}" : "Conversations" %></h1>
|
|
3
|
+
<div class="l-ui-container--spread">
|
|
4
|
+
<% if @assistant %>
|
|
5
|
+
<%= link_to "Back", layered_assistant.assistants_path, class: "l-ui-button--outline" %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<%= link_to "New", layered_assistant.new_conversation_path(conversation: @assistant ? { assistant_id: @assistant.id } : {}), class: "l-ui-button--primary" %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="l-ui-container--table l-ui-utility--mt-lg">
|
|
12
|
+
<table class="l-ui-table">
|
|
13
|
+
<caption class="l-ui-sr-only"><%= @assistant ? "Conversations for #{@assistant.name}" : "Conversations" %></caption>
|
|
14
|
+
<thead class="l-ui-table__header">
|
|
15
|
+
<tr>
|
|
16
|
+
<th scope="col" class="l-ui-table__header-cell">Name</th>
|
|
17
|
+
<% unless @assistant %>
|
|
18
|
+
<th scope="col" class="l-ui-table__header-cell">Assistant</th>
|
|
19
|
+
<% end %>
|
|
20
|
+
<th scope="col" class="l-ui-table__header-cell">Messages</th>
|
|
21
|
+
<th scope="col" class="l-ui-table__header-cell">Tokens</th>
|
|
22
|
+
<th scope="col" class="l-ui-table__header-cell">User</th>
|
|
23
|
+
<th scope="col" class="l-ui-table__header-cell">Created</th>
|
|
24
|
+
<th scope="col" class="l-ui-table__header-cell--action">Actions</th>
|
|
25
|
+
</tr>
|
|
26
|
+
</thead>
|
|
27
|
+
|
|
28
|
+
<tbody class="l-ui-table__body">
|
|
29
|
+
<% @conversations.each do |conversation| %>
|
|
30
|
+
<tr>
|
|
31
|
+
<th scope="row" class="l-ui-table__cell--primary">
|
|
32
|
+
<%= link_to conversation.name, layered_assistant.conversation_path(conversation) %>
|
|
33
|
+
</th>
|
|
34
|
+
<% unless @assistant %>
|
|
35
|
+
<td class="l-ui-table__cell">
|
|
36
|
+
<%= link_to conversation.assistant.name, layered_assistant.assistant_conversations_path(conversation.assistant) %>
|
|
37
|
+
</td>
|
|
38
|
+
<% end %>
|
|
39
|
+
<td class="l-ui-table__cell">
|
|
40
|
+
<%= link_to conversation.messages_count.to_i, layered_assistant.conversation_messages_path(conversation) %>
|
|
41
|
+
</td>
|
|
42
|
+
<td class="l-ui-table__cell">
|
|
43
|
+
<%= number_with_delimiter(conversation.token_estimate.to_i) %>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="l-ui-table__cell">
|
|
46
|
+
<%# nil owner = public conversation (guest user) %>
|
|
47
|
+
<%= conversation.owner.try(:name) || "Guest" %>
|
|
48
|
+
</td>
|
|
49
|
+
<td class="l-ui-table__cell">
|
|
50
|
+
<%= conversation.created_at.to_fs(:short) %>
|
|
51
|
+
</td>
|
|
52
|
+
<td class="l-ui-table__cell--action">
|
|
53
|
+
<%= link_to "Edit", layered_assistant.edit_conversation_path(conversation) %>
|
|
54
|
+
</td>
|
|
55
|
+
</tr>
|
|
56
|
+
<% end %>
|
|
57
|
+
</tbody>
|
|
58
|
+
</table>
|
|
59
|
+
|
|
60
|
+
<div class="l-ui-utility--mt-lg">
|
|
61
|
+
<%= l_ui_pagy(@pagy) %>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
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.conversations_path, 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 "layered/assistant/messages/system_prompt", conversation: @conversation %>
|
|
12
|
+
<%= render partial: "layered/assistant/messages/message", collection: @messages, as: :message %>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<button class="l-ui-scroll-to-bottom"
|
|
16
|
+
data-messages-target="scrollButton"
|
|
17
|
+
data-action="click->messages#jumpToBottom"
|
|
18
|
+
aria-label="Scroll to bottom"
|
|
19
|
+
type="button">
|
|
20
|
+
<%= image_tag "layered_ui/icon_chevron_down.svg", class: "l-ui-icon--sm l-ui-icon--dark-invert", alt: "" %>
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<%= render "layered/assistant/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id %>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%= form_with url: layered_assistant.conversation_messages_path(conversation), id: "composer-form", data: { controller: "composer", composer_target: "form", 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
|
+
|
|
8
|
+
<% if models.any? %>
|
|
9
|
+
<div class="l-ui-form__group">
|
|
10
|
+
<div class="l-ui-select-wrapper">
|
|
11
|
+
<%= f.select :model_id, options_for_select(models.map { |m| ["#{m.provider.name}: #{m.name}", m.id] }, selected_model_id), {}, name: "message[model_id]", class: "l-ui-select", "aria-label": "AI model" %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%= tag.div id: dom_id(message), class: "#{dom_id(message)} #{message.user? ? "l-ui-message--sent" : "l-ui-message"}" do %>
|
|
2
|
+
<div class="l-ui-message__bubble">
|
|
3
|
+
<% unless message.user? %>
|
|
4
|
+
<div class="l-ui-message__author"><%= message.role.capitalize %></div>
|
|
5
|
+
<% end %>
|
|
6
|
+
<div class="<%= dom_id(message) %>_body l-ui-message__body<%= " l-ui-markdown" unless message.assistant? && message.content.blank? %>">
|
|
7
|
+
<% if message.assistant? && message.content.blank? %>
|
|
8
|
+
<div class="l-ui-typing-indicator" role="status" aria-label="Assistant is typing">
|
|
9
|
+
<span class="l-ui-typing-indicator__dot"></span>
|
|
10
|
+
<span class="l-ui-typing-indicator__dot"></span>
|
|
11
|
+
<span class="l-ui-typing-indicator__dot"></span>
|
|
12
|
+
</div>
|
|
13
|
+
<% else %>
|
|
14
|
+
<%= render_message_content(message) %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="l-ui-message__footer">
|
|
18
|
+
<% total_tokens = message.input_tokens.to_i + message.output_tokens.to_i %>
|
|
19
|
+
<% if message.model || total_tokens > 0 %>
|
|
20
|
+
<span class="l-ui-message__metadata"><%= [message.model&.name, total_tokens > 0 ? "#{message.tokens_estimated? ? "~" : ""}#{number_with_delimiter(total_tokens)} tokens" : nil].compact.join(" · ") %></span>
|
|
21
|
+
<% end %>
|
|
22
|
+
<span class="l-ui-message__timestamp"><%= message.created_at.to_fs(:short) %></span>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% if conversation.assistant.system_prompt.present? %>
|
|
2
|
+
<div class="l-ui-message">
|
|
3
|
+
<div class="l-ui-message__bubble">
|
|
4
|
+
<div class="l-ui-message__author">System</div>
|
|
5
|
+
<div class="l-ui-message__body">
|
|
6
|
+
<%= conversation.assistant.system_prompt %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% if @error %>
|
|
2
|
+
<%= turbo_stream.append "#{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 "composer-form" do %>
|
|
8
|
+
<%= render "layered/assistant/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id %>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<div class="l-ui-container--spread">
|
|
2
|
+
<h1>Messages</h1>
|
|
3
|
+
<%= link_to "Back", layered_assistant.conversations_path, class: "l-ui-button--outline" %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="l-ui-container--table l-ui-utility--mt-lg">
|
|
7
|
+
<table class="l-ui-table">
|
|
8
|
+
<caption class="l-ui-sr-only">Messages</caption>
|
|
9
|
+
<thead class="l-ui-table__header">
|
|
10
|
+
<tr>
|
|
11
|
+
<th scope="col" class="l-ui-table__header-cell">Role</th>
|
|
12
|
+
<th scope="col" class="l-ui-table__header-cell">Content</th>
|
|
13
|
+
<th scope="col" class="l-ui-table__header-cell">Tokens</th>
|
|
14
|
+
<th scope="col" class="l-ui-table__header-cell">Created</th>
|
|
15
|
+
<th scope="col" class="l-ui-table__header-cell--action">Actions</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
|
|
19
|
+
<tbody class="l-ui-table__body">
|
|
20
|
+
<% @messages.each do |message| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<th scope="row" class="l-ui-table__cell--primary">
|
|
23
|
+
<%= message.role %>
|
|
24
|
+
</th>
|
|
25
|
+
<td class="l-ui-table__cell">
|
|
26
|
+
<%= truncate(message.content, length: 100) %>
|
|
27
|
+
</td>
|
|
28
|
+
<td class="l-ui-table__cell">
|
|
29
|
+
<% total = message.input_tokens.to_i + message.output_tokens.to_i %>
|
|
30
|
+
<%= number_with_delimiter(total) || 0 %>
|
|
31
|
+
</td>
|
|
32
|
+
<td class="l-ui-table__cell">
|
|
33
|
+
<%= message.created_at.to_fs(:short) %>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="l-ui-table__cell--action">
|
|
36
|
+
<%= link_to "Delete", layered_assistant.conversation_message_path(@conversation, message), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
42
|
+
|
|
43
|
+
<div class="l-ui-utility--mt-lg">
|
|
44
|
+
<%= l_ui_pagy(@pagy) %>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%= form_with(model: model, url: url, html: { class: "l-ui-form l-ui-utility--mt-2xl" }) do |f| %>
|
|
2
|
+
<%= render "layered_ui/shared/form_errors", item: model %>
|
|
3
|
+
|
|
4
|
+
<div class="l-ui-form__group">
|
|
5
|
+
<%= render "layered_ui/shared/label", form: f, field: :name, required: true %>
|
|
6
|
+
<%= f.text_field :name, class: "l-ui-form__field" %>
|
|
7
|
+
<%= render "layered_ui/shared/field_error", object: model, field: :name %>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="l-ui-form__group">
|
|
11
|
+
<%= render "layered_ui/shared/label", form: f, field: :identifier, required: true %>
|
|
12
|
+
<%= f.text_field :identifier, class: "l-ui-form__field" %>
|
|
13
|
+
<%= render "layered_ui/shared/field_error", object: model, field: :identifier %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<label class="l-ui-switch l-ui-utility--mt-xl">
|
|
17
|
+
<%= f.check_box :enabled, class: "l-ui-switch__input", role: "switch" %>
|
|
18
|
+
<span class="l-ui-switch__track"></span>
|
|
19
|
+
Enabled
|
|
20
|
+
</label>
|
|
21
|
+
|
|
22
|
+
<div class="l-ui-container--spread l-ui-utility--mt-2xl">
|
|
23
|
+
<% if model.persisted? %>
|
|
24
|
+
<%= link_to "Delete", layered_assistant.provider_model_path(@provider, model), class: "l-ui-button--outline-danger", data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
|
|
25
|
+
<% else %>
|
|
26
|
+
<span></span>
|
|
27
|
+
<% end %>
|
|
28
|
+
<%= f.submit f.object.new_record? ? 'Create' : 'Update', class: "l-ui-button--primary" %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<div class="l-ui-container--spread">
|
|
2
|
+
<h1>Edit Model</h1>
|
|
3
|
+
<%= link_to "Back", layered_assistant.provider_models_path(@provider), class: "l-ui-button--outline" %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= render "form", model: @model, url: layered_assistant.provider_model_path(@provider, @model) %>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<div class="l-ui-container--spread">
|
|
2
|
+
<h1>Models</h1>
|
|
3
|
+
<div class="l-ui-container--spread">
|
|
4
|
+
<%= link_to "Back", layered_assistant.providers_path, class: "l-ui-button--outline" %>
|
|
5
|
+
<%= link_to "New", layered_assistant.new_provider_model_path(@provider), class: "l-ui-button--primary" %>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="l-ui-container--table l-ui-utility--mt-lg">
|
|
10
|
+
<table class="l-ui-table">
|
|
11
|
+
<caption class="l-ui-sr-only">Models</caption>
|
|
12
|
+
<thead class="l-ui-table__header">
|
|
13
|
+
<tr>
|
|
14
|
+
<th scope="col" class="l-ui-table__header-cell">Name</th>
|
|
15
|
+
<th scope="col" class="l-ui-table__header-cell">Identifier</th>
|
|
16
|
+
<th scope="col" class="l-ui-table__header-cell">Enabled</th>
|
|
17
|
+
<th scope="col" class="l-ui-table__header-cell">Assistants</th>
|
|
18
|
+
<th scope="col" class="l-ui-table__header-cell">Messages</th>
|
|
19
|
+
<th scope="col" class="l-ui-table__header-cell--action">Actions</th>
|
|
20
|
+
</tr>
|
|
21
|
+
</thead>
|
|
22
|
+
|
|
23
|
+
<tbody class="l-ui-table__body">
|
|
24
|
+
<% @models.each do |model| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td class="l-ui-table__cell--primary">
|
|
27
|
+
<%= model.name %>
|
|
28
|
+
</td>
|
|
29
|
+
<td class="l-ui-table__cell">
|
|
30
|
+
<%= model.identifier %>
|
|
31
|
+
</td>
|
|
32
|
+
<td class="l-ui-table__cell">
|
|
33
|
+
<span class="<%= model.enabled? ? "l-ui-badge--success" : "l-ui-badge--danger" %>">
|
|
34
|
+
<%= model.enabled? ? "Enabled" : "Disabled" %>
|
|
35
|
+
</span>
|
|
36
|
+
</td>
|
|
37
|
+
<td class="l-ui-table__cell">
|
|
38
|
+
<%= model.assistants_count %>
|
|
39
|
+
</td>
|
|
40
|
+
<td class="l-ui-table__cell">
|
|
41
|
+
<%= model.messages_count %>
|
|
42
|
+
</td>
|
|
43
|
+
<td class="l-ui-table__cell--action">
|
|
44
|
+
<%= link_to "Edit", layered_assistant.edit_provider_model_path(@provider, model) %>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
<% end %>
|
|
48
|
+
</tbody>
|
|
49
|
+
</table>
|
|
50
|
+
|
|
51
|
+
<div class="l-ui-utility--mt-lg">
|
|
52
|
+
<%= l_ui_pagy(@pagy) %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
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_panel_conversation_path %>">
|
|
8
|
+
<option value="<%= layered_assistant.panel_conversations_path %>" <%= "selected" if selected == layered_assistant.panel_conversations_path %>>Conversations</option>
|
|
9
|
+
<option value="new" <%= "selected" if selected == "new" %>>+ New</option>
|
|
10
|
+
<% assistants.each do |assistant| %>
|
|
11
|
+
<% assistant_conversations = conversations.select { |c| c.assistant_id == assistant.id } %>
|
|
12
|
+
<% if assistant_conversations.any? %>
|
|
13
|
+
<optgroup label="<%= assistant.name %>">
|
|
14
|
+
<% assistant_conversations.each do |c| %>
|
|
15
|
+
<option value="<%= layered_assistant.panel_conversation_path(c) %>" <%= "selected" if selected == layered_assistant.panel_conversation_path(c) %>><%= c.name %></option>
|
|
16
|
+
<% end %>
|
|
17
|
+
</optgroup>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</select>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<%= render "layered/assistant/panel/conversations/header", assistants: @assistants, conversations: @conversations, selected: layered_assistant.panel_conversations_path %>
|
|
2
|
+
|
|
3
|
+
<%= turbo_frame_tag "assistant_panel" do %>
|
|
4
|
+
<div class="l-ui-container--table">
|
|
5
|
+
<table class="l-ui-table l-ui-utility--mt-0">
|
|
6
|
+
<caption class="l-ui-sr-only">Conversations in panel</caption>
|
|
7
|
+
<thead class="l-ui-table__header">
|
|
8
|
+
<tr>
|
|
9
|
+
<th scope="col" class="l-ui-table__header-cell">Created</th>
|
|
10
|
+
<th scope="col" class="l-ui-table__header-cell">Name</th>
|
|
11
|
+
<th scope="col" class="l-ui-table__header-cell">Messages</th>
|
|
12
|
+
<th scope="col" class="l-ui-table__header-cell">Tokens</th>
|
|
13
|
+
<th scope="col" class="l-ui-table__header-cell--action">Actions</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
|
|
17
|
+
<tbody class="l-ui-table__body">
|
|
18
|
+
<% if @conversations.any? %>
|
|
19
|
+
<% @conversations.each do |conversation| %>
|
|
20
|
+
<tr>
|
|
21
|
+
<td class="l-ui-table__cell">
|
|
22
|
+
<%= conversation.created_at.to_fs(:short) %>
|
|
23
|
+
</td>
|
|
24
|
+
<th scope="row" class="l-ui-table__cell--primary">
|
|
25
|
+
<%= link_to conversation.name, layered_assistant.panel_conversation_path(conversation), data: { turbo_frame: "assistant_panel" } %>
|
|
26
|
+
</th>
|
|
27
|
+
<td class="l-ui-table__cell">
|
|
28
|
+
<%= link_to conversation.messages_count || 0, layered_assistant.conversation_messages_path(conversation), data: { turbo_frame: "_top" } %>
|
|
29
|
+
</td>
|
|
30
|
+
<td class="l-ui-table__cell">
|
|
31
|
+
<%= number_with_delimiter(conversation.token_estimate.to_i) %>
|
|
32
|
+
</td>
|
|
33
|
+
<td class="l-ui-table__cell--action">
|
|
34
|
+
<%= button_to "Delete", layered_assistant.panel_conversation_path(conversation), method: :delete, class: "l-ui-table__action--danger", data: { turbo_confirm: "Delete this conversation?" } %>
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% else %>
|
|
39
|
+
<tr>
|
|
40
|
+
<td class="l-ui-table__cell" colspan="5">No conversations yet.</td>
|
|
41
|
+
</tr>
|
|
42
|
+
<% end %>
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%= render "layered/assistant/panel/conversations/header", assistants: @assistants, conversations: [], selected: "new" %>
|
|
2
|
+
|
|
3
|
+
<%= turbo_frame_tag "assistant_panel" do %>
|
|
4
|
+
<%= form_with(model: @conversation, url: layered_assistant.panel_conversations_path, html: { class: "l-ui-form l-ui-utility--mt-0" }) do |f| %>
|
|
5
|
+
<%= render "layered_ui/shared/form_errors", item: @conversation %>
|
|
6
|
+
|
|
7
|
+
<div class="l-ui-form__group l-ui-utility--mt-0">
|
|
8
|
+
<%= render "layered_ui/shared/label", form: f, field: :assistant_id, name: "Assistant", required: true %>
|
|
9
|
+
<%= f.select :assistant_id, @assistants.map { |a| [a.name, a.id] }, { include_blank: "Select:" }, class: "l-ui-select" %>
|
|
10
|
+
<%= render "layered_ui/shared/field_error", object: @conversation, field: :assistant %>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="l-ui-form__group">
|
|
14
|
+
<%= render "layered_ui/shared/label", form: f, field: :name, required: false %>
|
|
15
|
+
<%= f.text_field :name, class: "l-ui-form__field" %>
|
|
16
|
+
<p class="l-ui-form__hint">Auto-set from first message if left blank</p>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="l-ui-utility--mt-lg">
|
|
20
|
+
<%= f.submit "Create conversation", class: "l-ui-button--primary" %>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<%= render "layered/assistant/panel/conversations/header", assistants: @assistants, conversations: @conversations, selected: layered_assistant.panel_conversation_path(@conversation) %>
|
|
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="panel_<%= dom_id(@conversation) %>_messages" class="<%= dom_id(@conversation) %>_messages l-ui-conversation" aria-live="polite" data-messages-target="list">
|
|
9
|
+
<%= render "layered/assistant/messages/system_prompt", conversation: @conversation %>
|
|
10
|
+
<%= render partial: "layered/assistant/messages/message", collection: @messages, as: :message %>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<button class="l-ui-scroll-to-bottom"
|
|
14
|
+
data-messages-target="scrollButton"
|
|
15
|
+
data-action="click->messages#jumpToBottom"
|
|
16
|
+
aria-label="Scroll to bottom"
|
|
17
|
+
type="button">
|
|
18
|
+
<%= image_tag "layered_ui/icon_chevron_down.svg", class: "l-ui-icon--sm l-ui-icon--dark-invert", alt: "" %>
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<%= render "layered/assistant/panel/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id %>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%= form_with url: layered_assistant.panel_conversation_messages_path(conversation), id: "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
|
+
|
|
8
|
+
<% if models.any? %>
|
|
9
|
+
<div class="l-ui-form__group">
|
|
10
|
+
<div class="l-ui-select-wrapper">
|
|
11
|
+
<%= f.select :model_id, options_for_select(models.map { |m| ["#{m.provider.name}: #{m.name}", m.id] }, selected_model_id), {}, name: "message[model_id]", class: "l-ui-select", "aria-label": "AI model" %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% 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 "panel-composer-form" do %>
|
|
8
|
+
<%= render "layered/assistant/panel/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id %>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<%= form_with(model: provider, url: url, html: { class: "l-ui-form l-ui-utility--mt-2xl" }) do |f| %>
|
|
2
|
+
<%= render "layered_ui/shared/form_errors", item: provider %>
|
|
3
|
+
|
|
4
|
+
<div data-controller="provider-template" data-provider-template-templates-value="<%= Layered::Assistant::Provider::TEMPLATES.values.flatten.to_json %>">
|
|
5
|
+
<% unless provider.persisted? %>
|
|
6
|
+
<div class="l-ui-form__group">
|
|
7
|
+
<label class="l-ui-label" for="provider_template">Template</label>
|
|
8
|
+
<select id="provider_template" class="l-ui-select" data-action="provider-template#apply">
|
|
9
|
+
<option value="">Manual</option>
|
|
10
|
+
<% Layered::Assistant::Provider::TEMPLATES.each do |group, templates| %>
|
|
11
|
+
<optgroup label="<%= group %>">
|
|
12
|
+
<% templates.each do |t| %>
|
|
13
|
+
<option value="<%= t[:key] %>"><%= t[:name] %></option>
|
|
14
|
+
<% end %>
|
|
15
|
+
</optgroup>
|
|
16
|
+
<% end %>
|
|
17
|
+
</select>
|
|
18
|
+
<div class="l-ui-form__hint" hidden data-provider-template-target="description"></div>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<div class="l-ui-form__group">
|
|
23
|
+
<%= render "layered_ui/shared/label", form: f, field: :name, required: true %>
|
|
24
|
+
<%= f.text_field :name, class: "l-ui-form__field", data: { provider_template_target: "name" } %>
|
|
25
|
+
<%= render "layered_ui/shared/field_error", object: provider, field: :name %>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="l-ui-form__group">
|
|
29
|
+
<%= render "layered_ui/shared/label", form: f, field: :protocol, required: true %>
|
|
30
|
+
<%= f.select :protocol, Layered::Assistant::Provider.protocols.map { |k, v| [v, k] }, { include_blank: "Select:" }, { class: "l-ui-select", data: { provider_template_target: "protocol" } } %>
|
|
31
|
+
<%= render "layered_ui/shared/field_error", object: provider, field: :protocol %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="l-ui-form__group">
|
|
35
|
+
<%= render "layered_ui/shared/label", form: f, field: :url, name: "URL", required: false %>
|
|
36
|
+
<%= f.url_field :url, class: "l-ui-form__field", data: { provider_template_target: "url" } %>
|
|
37
|
+
<%= render "layered_ui/shared/field_error", object: provider, field: :url %>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="l-ui-form__group">
|
|
41
|
+
<%= render "layered_ui/shared/label", form: f, field: :secret, name: "Secret", required: false %>
|
|
42
|
+
<%= f.password_field :secret, class: "l-ui-form__field" %>
|
|
43
|
+
<%= render "layered_ui/shared/field_error", object: provider, field: :secret %>
|
|
44
|
+
<div class="l-ui-form__hint" hidden data-provider-template-target="secretHint"></div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<label class="l-ui-switch l-ui-utility--mt-xl">
|
|
49
|
+
<%= f.check_box :enabled, class: "l-ui-switch__input", role: "switch" %>
|
|
50
|
+
<span class="l-ui-switch__track"></span>
|
|
51
|
+
Enabled
|
|
52
|
+
</label>
|
|
53
|
+
|
|
54
|
+
<% unless provider.persisted? %>
|
|
55
|
+
<div class="l-ui-form__group">
|
|
56
|
+
<label class="l-ui-label">Models</label>
|
|
57
|
+
<div class="l-ui-radio__group">
|
|
58
|
+
<div class="l-ui-radio__item">
|
|
59
|
+
<%= f.radio_button :create_models, "1", class: "l-ui-radio__input", checked: true %>
|
|
60
|
+
<%= f.label :create_models_1, "If available, automatically create popular models for this provider", class: "l-ui-radio__label" %>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="l-ui-radio__item">
|
|
63
|
+
<%= f.radio_button :create_models, "0", class: "l-ui-radio__input" %>
|
|
64
|
+
<%= f.label :create_models_0, "Add your own models manually", class: "l-ui-radio__label" %>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="l-ui-form__hint">
|
|
68
|
+
Popular model identifiers are sourced from: <%= link_to "https://github.com/layered-ai-public/layered-assistant-rails/blob/main/data/models.json", "https://github.com/layered-ai-public/layered-assistant-rails/blob/main/data/models.json", target: "_blank", rel: "noopener noreferrer" %>.
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
|
|
73
|
+
<div class="l-ui-container--spread l-ui-utility--mt-2xl">
|
|
74
|
+
<% if provider.persisted? %>
|
|
75
|
+
<%= link_to "Delete", layered_assistant.provider_path(provider), class: "l-ui-button--outline-danger", data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
|
|
76
|
+
<% else %>
|
|
77
|
+
<span></span>
|
|
78
|
+
<% end %>
|
|
79
|
+
<%= f.submit f.object.new_record? ? 'Create' : 'Update', class: "l-ui-button--primary" %>
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<div class="l-ui-container--spread">
|
|
2
|
+
<h1>Providers</h1>
|
|
3
|
+
<%= link_to "New", layered_assistant.new_provider_path, class: "l-ui-button--primary" %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="l-ui-container--table l-ui-utility--mt-lg">
|
|
7
|
+
<table class="l-ui-table">
|
|
8
|
+
<caption class="l-ui-sr-only">Providers</caption>
|
|
9
|
+
<thead class="l-ui-table__header">
|
|
10
|
+
<tr>
|
|
11
|
+
<th scope="col" class="l-ui-table__header-cell">Name</th>
|
|
12
|
+
<th scope="col" class="l-ui-table__header-cell">Protocol</th>
|
|
13
|
+
<th scope="col" class="l-ui-table__header-cell">Models</th>
|
|
14
|
+
<th scope="col" class="l-ui-table__header-cell">Enabled</th>
|
|
15
|
+
<th scope="col" class="l-ui-table__header-cell--action">Actions</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
|
|
19
|
+
<tbody class="l-ui-table__body">
|
|
20
|
+
<% @providers.each do |provider| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td class="l-ui-table__cell--primary">
|
|
23
|
+
<%= provider.name %>
|
|
24
|
+
</td>
|
|
25
|
+
<td class="l-ui-table__cell">
|
|
26
|
+
<%= Layered::Assistant::Provider.protocols[provider.protocol] %>
|
|
27
|
+
</td>
|
|
28
|
+
<td class="l-ui-table__cell">
|
|
29
|
+
<%= link_to provider.models_count, layered_assistant.provider_models_path(provider) %>
|
|
30
|
+
</td>
|
|
31
|
+
<td class="l-ui-table__cell">
|
|
32
|
+
<span class="<%= provider.enabled? ? "l-ui-badge--success" : "l-ui-badge--danger" %>">
|
|
33
|
+
<%= provider.enabled? ? "Enabled" : "Disabled" %>
|
|
34
|
+
</span>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="l-ui-table__cell--action">
|
|
37
|
+
<%= link_to "Edit", layered_assistant.edit_provider_path(provider) %>
|
|
38
|
+
</td>
|
|
39
|
+
</tr>
|
|
40
|
+
<% end %>
|
|
41
|
+
</tbody>
|
|
42
|
+
</table>
|
|
43
|
+
|
|
44
|
+
<div class="l-ui-utility--mt-lg">
|
|
45
|
+
<%= l_ui_pagy(@pagy) %>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|