llm_meta_client 1.4.0 → 1.5.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 +4 -4
- data/CHANGELOG.md +27 -0
- data/lib/generators/llm_meta_client/scaffold/scaffold_generator.rb +12 -7
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/api/mcp_servers_controller.rb +2 -2
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chat_streams_controller.rb +24 -2
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb +92 -76
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/prompts_controller.rb +28 -1
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/asset_actions_controller.js +98 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_controller.js +126 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_menu_controller.js +42 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_title_edit_controller.js +5 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chats_form_controller.js +186 -12
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/generation_settings_controller.js +38 -20
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/input_controls_controller.js +55 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/llm_toggle_controller.js +27 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/message_stream_controller.js +102 -3
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/model_picker_controller.js +160 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/tool_selector_controller.js +10 -2
- data/lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb +130 -44
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_chat_sidebar.html.erb +3 -1
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_message.html.erb +3 -1
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_streaming_message.html.erb +6 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_tool_call_message.html.erb +20 -18
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb +31 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/destroy.turbo_stream.erb +3 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/edit.html.erb +53 -17
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/new.html.erb +50 -17
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/_header.html.erb +1 -5
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/_new_chat_button.html.erb +7 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/application.html.erb +2 -2
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_generation_settings_field.html.erb +7 -5
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_model_grid.html.erb +88 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_quick_picks.html.erb +67 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_tool_selector_field.html.erb +1 -1
- data/lib/llm_meta_client/helpers.rb +18 -0
- data/lib/llm_meta_client/server_query.rb +24 -6
- data/lib/llm_meta_client/version.rb +1 -1
- metadata +11 -6
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/llm_selector_controller.js +0 -236
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/update.turbo_stream.erb +0 -85
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_api_key_field.html.erb +0 -15
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_family_field.html.erb +0 -18
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_model_field.html.erb +0 -12
data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb
CHANGED
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
<%%= render partial: "chats/chat_sidebar", locals: { chat: @chat } %>
|
|
29
29
|
<%% end %>
|
|
30
30
|
|
|
31
|
+
<%% # Reveal/refresh the "New Chat" header button now that the chat has content %>
|
|
32
|
+
<%%= turbo_stream.replace "new-chat-button-slot" do %>
|
|
33
|
+
<%%= render partial: "layouts/new_chat_button" %>
|
|
34
|
+
<%% end %>
|
|
35
|
+
|
|
31
36
|
<%% # Update history sidebar - replace entire content to ensure update %>
|
|
32
37
|
<%% if @prompt_execution %>
|
|
33
38
|
<%%= turbo_stream.replace "history-sidebar" do %>
|
|
@@ -66,6 +71,16 @@
|
|
|
66
71
|
messageInput.focus();
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
// Clear the image-preview box + file input now that the multipart
|
|
75
|
+
// form has been sent. Doing it in the JS submit handler would empty
|
|
76
|
+
// the file input before the browser serialized the POST body.
|
|
77
|
+
const imagePreview = document.querySelector('[data-chats-form-target="imagePreview"]');
|
|
78
|
+
if (imagePreview) imagePreview.style.display = 'none';
|
|
79
|
+
const imageThumbnail = document.querySelector('[data-chats-form-target="imageThumbnail"]');
|
|
80
|
+
if (imageThumbnail) imageThumbnail.src = '';
|
|
81
|
+
const imageInput = document.querySelector('[data-chats-form-target="imageInput"]');
|
|
82
|
+
if (imageInput) imageInput.value = '';
|
|
83
|
+
|
|
69
84
|
// Update submit button state
|
|
70
85
|
const form = document.querySelector('[data-controller="chats-form"]');
|
|
71
86
|
if (form && messageInput) {
|
|
@@ -79,6 +94,22 @@
|
|
|
79
94
|
branchField.value = '<%%= @prompt_execution&.execution_id %>';
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
// Make this tab's chat identity URL-local so cross-tab navigation
|
|
98
|
+
// can't silently re-target subsequent prompts. After the first POST
|
|
99
|
+
// /chats, swap the URL bar to /chats/<uuid> and point the form at
|
|
100
|
+
// /chats/<uuid>/add_prompt for all subsequent prompts.
|
|
101
|
+
const chatUuid = '<%%= @chat&.uuid %>';
|
|
102
|
+
if (chatUuid) {
|
|
103
|
+
const desiredPath = `/chats/${chatUuid}`;
|
|
104
|
+
if (window.location.pathname !== desiredPath) {
|
|
105
|
+
history.replaceState({}, '', desiredPath);
|
|
106
|
+
}
|
|
107
|
+
const chatForm = document.querySelector('.chat-form');
|
|
108
|
+
if (chatForm) {
|
|
109
|
+
chatForm.action = `/chats/${chatUuid}/add_prompt`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
// Scroll to bottom
|
|
83
114
|
const chatMessages = document.getElementById('chat-messages');
|
|
84
115
|
if (chatMessages) {
|
|
@@ -8,30 +8,33 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<div class="chat-input-container">
|
|
11
|
-
<%%= form_with url:
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</
|
|
18
|
-
|
|
19
|
-
<%%= render "shared/tool_selector_field", stimulus_controller: "tool-selector" %>
|
|
20
|
-
<%% end %>
|
|
21
|
-
<%%= render "shared/generation_settings_field", stimulus_controller: "generation-settings" %>
|
|
22
|
-
<%% end %>
|
|
11
|
+
<%%= form_with url: add_prompt_chat_path(@chat.uuid), method: :post, class: "chat-form", data: { controller: "chats-form model-picker", guest: current_user.nil?, action: "submit->chats-form#submit model-picker:changed->chats-form#updateSubmitButton dragover->chats-form#onDragOver dragleave->chats-form#onDragLeave drop->chats-form#onDrop" } do |f| %>
|
|
12
|
+
<div class="image-preview" data-chats-form-target="imagePreview" style="display: none;">
|
|
13
|
+
<img data-chats-form-target="imageThumbnail" alt="Attached image preview" />
|
|
14
|
+
<button type="button" class="image-clear-button" title="Remove image"
|
|
15
|
+
data-action="click->chats-form#clearImage">
|
|
16
|
+
<i class="bi bi-x-circle"></i>
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
23
19
|
<div class="input-wrapper">
|
|
24
20
|
<%%= f.text_area :message,
|
|
25
21
|
placeholder: "Enter your message...",
|
|
26
22
|
class: "chat-input",
|
|
27
|
-
rows:
|
|
23
|
+
rows: 2,
|
|
28
24
|
autofocus: true,
|
|
29
25
|
required: true,
|
|
30
26
|
id: "message-input",
|
|
31
|
-
data: { "chats-form-target": "prompt", action: "input->chats-form#updateSubmitButton" } %>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
data: { "chats-form-target": "prompt", action: "input->chats-form#updateSubmitButton keydown->chats-form#onPromptKeydown paste->chats-form#onPaste" } %>
|
|
28
|
+
<button type="button" class="attach-button" disabled
|
|
29
|
+
title="Attach image"
|
|
30
|
+
data-chats-form-target="attachButton"
|
|
31
|
+
data-action="click->chats-form#openImagePicker">
|
|
32
|
+
<i class="bi bi-paperclip"></i>
|
|
33
|
+
</button>
|
|
34
|
+
<%%= f.file_field :image, accept: "image/*",
|
|
35
|
+
data: { "chats-form-target": "imageInput",
|
|
36
|
+
action: "change->chats-form#onImageSelected" },
|
|
37
|
+
style: "display: none;" %>
|
|
35
38
|
<%%= f.button type: "submit",
|
|
36
39
|
class: "send-button",
|
|
37
40
|
disabled: true,
|
|
@@ -41,6 +44,39 @@
|
|
|
41
44
|
<i class="bi bi-send-fill"></i>
|
|
42
45
|
<%% end %>
|
|
43
46
|
</div>
|
|
47
|
+
<%% if @llm_families.present? %>
|
|
48
|
+
<%%= render "shared/quick_picks", llm_families: @llm_families, stimulus_controller: "model-picker" %>
|
|
49
|
+
|
|
50
|
+
<%%# Pre-populate from the chat's most recent execution so the picker
|
|
51
|
+
opens on the model the chat was last using rather than the system
|
|
52
|
+
default. JS treats a non-blank `model` value as the initial pick. %>
|
|
53
|
+
<%%= hidden_field_tag :family, @prompt_execution&.llm_platform.to_s, data: { "model-picker-target": "family" } %>
|
|
54
|
+
<%%= hidden_field_tag :api_key_uuid, @prompt_execution&.llm_uuid.to_s, data: { "model-picker-target": "apiKey" } %>
|
|
55
|
+
<%%= hidden_field_tag :model, @prompt_execution&.model.to_s, data: { "model-picker-target": "model" }, required: true %>
|
|
56
|
+
|
|
57
|
+
<div class="input-controls" data-controller="input-controls">
|
|
58
|
+
<div class="input-controls-buttons">
|
|
59
|
+
<div class="llm-toggle-field" data-controller="llm-toggle">
|
|
60
|
+
<button type="button"
|
|
61
|
+
class="llm-toggle-button"
|
|
62
|
+
data-llm-toggle-target="toggleButton"
|
|
63
|
+
data-action="click->llm-toggle#toggle">
|
|
64
|
+
<i class="bi bi-grid-3x3-gap"></i>
|
|
65
|
+
<span data-llm-toggle-target="label">Other models</span>
|
|
66
|
+
<i class="bi bi-chevron-up toggle-icon" data-llm-toggle-target="toggleIcon"></i>
|
|
67
|
+
</button>
|
|
68
|
+
<div class="llm-toggle-panel" data-llm-toggle-target="panel" style="display: none;">
|
|
69
|
+
<%%= render "shared/model_grid", llm_families: @llm_families, stimulus_controller: "model-picker" %>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<%% if user_signed_in? %>
|
|
73
|
+
<%%= render "shared/tool_selector_field", stimulus_controller: "tool-selector" %>
|
|
74
|
+
<%% end %>
|
|
75
|
+
<%%= render "shared/generation_settings_field", stimulus_controller: "generation-settings" %>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<%% end %>
|
|
79
|
+
<%%= f.hidden_field :branch_from_uuid, value: @branch_from_uuid || params.dig(:chat, :branch_from_uuid) %>
|
|
44
80
|
<%% end %>
|
|
45
81
|
</div>
|
|
46
82
|
</div>
|
|
@@ -8,30 +8,33 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<div class="chat-input-container">
|
|
11
|
-
<%%= form_with url: chats_path, method: :post, class: "chat-form", data: { controller: "chats-form
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</
|
|
18
|
-
|
|
19
|
-
<%%= render "shared/tool_selector_field", stimulus_controller: "tool-selector" %>
|
|
20
|
-
<%% end %>
|
|
21
|
-
<%%= render "shared/generation_settings_field", stimulus_controller: "generation-settings" %>
|
|
22
|
-
<%% end %>
|
|
11
|
+
<%%= form_with url: chats_path, method: :post, class: "chat-form", data: { controller: "chats-form model-picker", guest: current_user.nil?, action: "submit->chats-form#submit model-picker:changed->chats-form#updateSubmitButton dragover->chats-form#onDragOver dragleave->chats-form#onDragLeave drop->chats-form#onDrop" } do |f| %>
|
|
12
|
+
<div class="image-preview" data-chats-form-target="imagePreview" style="display: none;">
|
|
13
|
+
<img data-chats-form-target="imageThumbnail" alt="Attached image preview" />
|
|
14
|
+
<button type="button" class="image-clear-button" title="Remove image"
|
|
15
|
+
data-action="click->chats-form#clearImage">
|
|
16
|
+
<i class="bi bi-x-circle"></i>
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
23
19
|
<div class="input-wrapper">
|
|
24
20
|
<%%= f.text_area :message,
|
|
25
21
|
placeholder: "Enter your message...",
|
|
26
22
|
class: "chat-input",
|
|
27
|
-
rows:
|
|
23
|
+
rows: 2,
|
|
28
24
|
autofocus: true,
|
|
29
25
|
required: true,
|
|
30
26
|
id: "message-input",
|
|
31
|
-
data: { "chats-form-target": "prompt", action: "input->chats-form#updateSubmitButton" } %>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
data: { "chats-form-target": "prompt", action: "input->chats-form#updateSubmitButton keydown->chats-form#onPromptKeydown paste->chats-form#onPaste" } %>
|
|
28
|
+
<button type="button" class="attach-button" disabled
|
|
29
|
+
title="Attach image"
|
|
30
|
+
data-chats-form-target="attachButton"
|
|
31
|
+
data-action="click->chats-form#openImagePicker">
|
|
32
|
+
<i class="bi bi-paperclip"></i>
|
|
33
|
+
</button>
|
|
34
|
+
<%%= f.file_field :image, accept: "image/*",
|
|
35
|
+
data: { "chats-form-target": "imageInput",
|
|
36
|
+
action: "change->chats-form#onImageSelected" },
|
|
37
|
+
style: "display: none;" %>
|
|
35
38
|
<%%= f.button type: "submit",
|
|
36
39
|
class: "send-button",
|
|
37
40
|
disabled: true,
|
|
@@ -41,6 +44,36 @@
|
|
|
41
44
|
<i class="bi bi-send-fill"></i>
|
|
42
45
|
<%% end %>
|
|
43
46
|
</div>
|
|
47
|
+
<%% if @llm_families.present? %>
|
|
48
|
+
<%%= render "shared/quick_picks", llm_families: @llm_families, stimulus_controller: "model-picker" %>
|
|
49
|
+
|
|
50
|
+
<%%= hidden_field_tag :family, "", data: { "model-picker-target": "family" } %>
|
|
51
|
+
<%%= hidden_field_tag :api_key_uuid, "", data: { "model-picker-target": "apiKey" } %>
|
|
52
|
+
<%%= hidden_field_tag :model, "", data: { "model-picker-target": "model" }, required: true %>
|
|
53
|
+
|
|
54
|
+
<div class="input-controls" data-controller="input-controls">
|
|
55
|
+
<div class="input-controls-buttons">
|
|
56
|
+
<div class="llm-toggle-field" data-controller="llm-toggle">
|
|
57
|
+
<button type="button"
|
|
58
|
+
class="llm-toggle-button"
|
|
59
|
+
data-llm-toggle-target="toggleButton"
|
|
60
|
+
data-action="click->llm-toggle#toggle">
|
|
61
|
+
<i class="bi bi-grid-3x3-gap"></i>
|
|
62
|
+
<span data-llm-toggle-target="label">Other models</span>
|
|
63
|
+
<i class="bi bi-chevron-up toggle-icon" data-llm-toggle-target="toggleIcon"></i>
|
|
64
|
+
</button>
|
|
65
|
+
<div class="llm-toggle-panel" data-llm-toggle-target="panel" style="display: none;">
|
|
66
|
+
<%%= render "shared/model_grid", llm_families: @llm_families, stimulus_controller: "model-picker" %>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<%% if user_signed_in? %>
|
|
70
|
+
<%%= render "shared/tool_selector_field", stimulus_controller: "tool-selector" %>
|
|
71
|
+
<%% end %>
|
|
72
|
+
<%%= render "shared/generation_settings_field", stimulus_controller: "generation-settings" %>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<%% end %>
|
|
76
|
+
<%%= f.hidden_field :branch_from_uuid, value: @branch_from_uuid || params.dig(:chat, :branch_from_uuid) %>
|
|
44
77
|
<%% end %>
|
|
45
78
|
</div>
|
|
46
79
|
</div>
|
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
<%%= link_to "LLM Meta Client", root_path %>
|
|
5
5
|
</div>
|
|
6
6
|
<div class="header-right">
|
|
7
|
-
|
|
8
|
-
<%%= button_to start_new_chats_path, method: :post, class: "new-text-button" do %>
|
|
9
|
-
<i class="fa-solid fa-plus"></i> New Chat
|
|
10
|
-
<%% end %>
|
|
11
|
-
<%% end %>
|
|
7
|
+
<%%= render partial: "layouts/new_chat_button" %>
|
|
12
8
|
<%% if user_signed_in? %>
|
|
13
9
|
<div class="user-menu">
|
|
14
10
|
<button popovertarget="user-menu-popover" class="user-menu-toggle" type="button">
|
data/lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/_new_chat_button.html.erb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div id="new-chat-button-slot" class="new-chat-button-slot">
|
|
2
|
+
<%% if @messages.present? || @user_message.present? %>
|
|
3
|
+
<%%= button_to start_new_chats_path, method: :post, class: "new-text-button" do %>
|
|
4
|
+
<i class="fa-solid fa-plus"></i> New Chat
|
|
5
|
+
<%% end %>
|
|
6
|
+
<%% end %>
|
|
7
|
+
</div>
|
data/lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/application.html.erb
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<%% if user_signed_in? %>
|
|
33
33
|
<div class="sidebar sidebar-left">
|
|
34
34
|
<div id="chat-sidebar">
|
|
35
|
-
<%%= chat_list(->(id) { chat_path(id) }, active_uuid: @active_chat_uuid, download_csv_path: ->(id) { download_csv_chat_path(id) },
|
|
35
|
+
<%%= chat_list(->(id) { chat_path(id) }, active_uuid: @active_chat_uuid, download_csv_path: ->(id) { download_csv_chat_path(id) }, delete_path: ->(id) { chat_path(id) }, batch_delete_path: batch_destroy_chats_path, batch_download_csv_path: download_selected_csv_chats_path) %>
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
38
38
|
<%% end %>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
</div>
|
|
52
52
|
<div class="sidebar sidebar-right">
|
|
53
53
|
<div id="history-sidebar">
|
|
54
|
-
<%%= history_list(->(execution_id) { prompt_path(execution_id) }, active_uuid: @active_message_uuid) %>
|
|
54
|
+
<%%= history_list(->(execution_id) { prompt_path(execution_id) }, active_uuid: @active_message_uuid, delete_path: ->(execution_id) { prompt_path(execution_id) }) %>
|
|
55
55
|
</div>
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
data-<%%= stimulus_controller %>-target="toggleButton"
|
|
9
9
|
data-action="click-><%%= stimulus_controller %>#toggle">
|
|
10
10
|
<i class="bi bi-sliders"></i>
|
|
11
|
-
|
|
12
|
-
<
|
|
11
|
+
Settings
|
|
12
|
+
<span class="tool-count-badge" data-<%%= stimulus_controller %>-target="countBadge" style="display: none;">0</span>
|
|
13
|
+
<i class="bi bi-chevron-up toggle-icon" data-<%%= stimulus_controller %>-target="toggleIcon"></i>
|
|
13
14
|
</button>
|
|
14
15
|
</div>
|
|
15
16
|
<div class="generation-settings-panel" data-<%%= stimulus_controller %>-target="panel" style="display: none;">
|
|
@@ -18,13 +19,14 @@
|
|
|
18
19
|
</label>
|
|
19
20
|
<textarea name="generation_settings_json" id="generation_settings_json"
|
|
20
21
|
class="generation-settings-json-input"
|
|
21
|
-
rows="
|
|
22
|
-
placeholder='{"temperature": 0.7, "
|
|
22
|
+
rows="3"
|
|
23
|
+
placeholder='{"temperature": 0.7, "max_tokens": 4096, "think": true, "options": {"num_ctx": 8192}}'
|
|
23
24
|
data-<%%= stimulus_controller %>-target="jsonInput"
|
|
24
25
|
data-action="input-><%%= stimulus_controller %>#validate"></textarea>
|
|
25
26
|
<div class="generation-settings-error" data-<%%= stimulus_controller %>-target="error" style="display: none;"></div>
|
|
26
27
|
<div class="generation-settings-hint">
|
|
27
|
-
|
|
28
|
+
Any JSON keys/values — passed through to the provider. Common keys:
|
|
29
|
+
temperature, top_k, top_p, max_tokens, repeat_penalty, think, options.
|
|
28
30
|
</div>
|
|
29
31
|
</div>
|
|
30
32
|
</div>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<%%
|
|
2
|
+
llm_families = local_assigns[:llm_families] || []
|
|
3
|
+
stimulus_controller = local_assigns[:stimulus_controller] || "model-picker"
|
|
4
|
+
|
|
5
|
+
# Build one column per (family × api_key) tuple. "Free" (Ollama) is
|
|
6
|
+
# always shown first when available; brand-named columns follow.
|
|
7
|
+
brand_names = { "anthropic" => "Claude", "google" => "Gemini", "openai" => "GPT" }
|
|
8
|
+
ordered_families = [
|
|
9
|
+
llm_families.find { |f| f[:llm_type] == "ollama" }
|
|
10
|
+
] + brand_names.keys.map { |t| llm_families.find { |f| f[:llm_type] == t } }
|
|
11
|
+
|
|
12
|
+
columns = ordered_families.compact.flat_map do |fam|
|
|
13
|
+
Array(fam[:api_keys]).map do |key|
|
|
14
|
+
base_title = (fam[:llm_type] == "ollama") ? "Free" : brand_names.fetch(fam[:llm_type], fam[:name].to_s)
|
|
15
|
+
# Strip the "[Provider] " prefix that the meta-server adds to the
|
|
16
|
+
# description so the column header reads cleanly.
|
|
17
|
+
desc = key[:description].to_s.sub(/\A\[[^\]]*\]\s*/, "")
|
|
18
|
+
multiple_keys = Array(fam[:api_keys]).size > 1
|
|
19
|
+
title = (multiple_keys && desc.present?) ? "#{base_title} (#{desc})" : base_title
|
|
20
|
+
{
|
|
21
|
+
title: title,
|
|
22
|
+
family: fam[:llm_type],
|
|
23
|
+
api_key_uuid: key[:uuid],
|
|
24
|
+
models: Array(key[:available_models])
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
%>
|
|
29
|
+
|
|
30
|
+
<div class="model-grid">
|
|
31
|
+
<%% if columns.any? %>
|
|
32
|
+
<div class="model-grid-columns" style="--cols: <%%= columns.size %>;">
|
|
33
|
+
<%% columns.each do |col| %>
|
|
34
|
+
<div class="model-grid-column">
|
|
35
|
+
<div class="model-grid-header"><%%= col[:title] %></div>
|
|
36
|
+
<ul class="model-grid-list">
|
|
37
|
+
<%% col[:models].each do |m| %>
|
|
38
|
+
<li>
|
|
39
|
+
<button type="button"
|
|
40
|
+
class="model-grid-cell"
|
|
41
|
+
title="<%%= m['value'] %>"
|
|
42
|
+
data-family="<%%= col[:family] %>"
|
|
43
|
+
data-api-key-uuid="<%%= col[:api_key_uuid] %>"
|
|
44
|
+
data-model="<%%= m['value'] %>"
|
|
45
|
+
data-supports-vision="<%%= m['supports_vision'] %>"
|
|
46
|
+
data-action="click-><%%= stimulus_controller %>#pick">
|
|
47
|
+
<span class="model-grid-label"><%%= m['label'] %></span>
|
|
48
|
+
<span class="model-grid-badges">
|
|
49
|
+
<%% if m['kind'].to_s == "image" %>
|
|
50
|
+
<i class="bi bi-image model-badge model-badge-image" title="Image generation"></i>
|
|
51
|
+
<%% end %>
|
|
52
|
+
<%% if m['supports_vision'] %>
|
|
53
|
+
<i class="bi bi-eye model-badge model-badge-vision" title="Vision input"></i>
|
|
54
|
+
<%% end %>
|
|
55
|
+
<%% if m['supports_tools'] %>
|
|
56
|
+
<i class="bi bi-tools model-badge model-badge-tools" title="Tool / function calling"></i>
|
|
57
|
+
<%% end %>
|
|
58
|
+
<%% if m['favorite'] %>
|
|
59
|
+
<i class="bi bi-star-fill model-badge model-badge-favorite" title="One of your favorites"></i>
|
|
60
|
+
<%% end %>
|
|
61
|
+
<%% if m['default'] %>
|
|
62
|
+
<i class="bi bi-bookmark-star-fill model-badge model-badge-default" title="Your default model"></i>
|
|
63
|
+
<%% end %>
|
|
64
|
+
</span>
|
|
65
|
+
</button>
|
|
66
|
+
</li>
|
|
67
|
+
<%% end %>
|
|
68
|
+
</ul>
|
|
69
|
+
</div>
|
|
70
|
+
<%% end %>
|
|
71
|
+
</div>
|
|
72
|
+
<%% else %>
|
|
73
|
+
<p class="model-grid-empty">No models available.</p>
|
|
74
|
+
<%% end %>
|
|
75
|
+
|
|
76
|
+
<p class="model-grid-footer">
|
|
77
|
+
<i class="bi bi-info-circle"></i>
|
|
78
|
+
<%% if user_signed_in? %>
|
|
79
|
+
Access
|
|
80
|
+
<%%= link_to "hub.AIbranch",
|
|
81
|
+
Rails.configuration.llm_service_public_url,
|
|
82
|
+
target: "_blank", rel: "noopener" %>
|
|
83
|
+
to unlock more models by registering your API keys.
|
|
84
|
+
<%% else %>
|
|
85
|
+
Sign in to add API keys, favorites, and set a personal default model.
|
|
86
|
+
<%% end %>
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<%%
|
|
2
|
+
llm_families = local_assigns[:llm_families] || []
|
|
3
|
+
stimulus_controller = local_assigns[:stimulus_controller] || "model-picker"
|
|
4
|
+
|
|
5
|
+
# Flatten every (family × api_key × model) combination so the picker
|
|
6
|
+
# can find both the user's default and their favorites in one pass.
|
|
7
|
+
all_options = llm_families.flat_map do |fam|
|
|
8
|
+
Array(fam[:api_keys]).flat_map do |key|
|
|
9
|
+
Array(key[:available_models]).map do |m|
|
|
10
|
+
{
|
|
11
|
+
family: fam[:llm_type],
|
|
12
|
+
api_key_uuid: key[:uuid],
|
|
13
|
+
meta_id: m["value"],
|
|
14
|
+
label: m["label"],
|
|
15
|
+
favorite: m["favorite"] == true,
|
|
16
|
+
default: m["default"] == true,
|
|
17
|
+
supports_vision: m["supports_vision"] == true,
|
|
18
|
+
kind: m["kind"]
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The user's per-user default (set on the meta-server's /models page) wins.
|
|
25
|
+
# If they haven't set one, fall back to the system-wide default from config.
|
|
26
|
+
system_default_meta = Rails.configuration.default_model
|
|
27
|
+
default_option = all_options.find { |o| o[:default] } ||
|
|
28
|
+
all_options.find { |o| o[:meta_id] == system_default_meta }
|
|
29
|
+
|
|
30
|
+
# Favorites that aren't already the default (to avoid showing the same
|
|
31
|
+
# model twice in the row). Anonymous visitors have no favorites — they
|
|
32
|
+
# only see the free (Ollama) family — so promote every available model
|
|
33
|
+
# to a quick-pick for them, since picking from the grid would be the
|
|
34
|
+
# only other option.
|
|
35
|
+
favorite_options = all_options.reject { |o| o[:meta_id] == default_option&.dig(:meta_id) }
|
|
36
|
+
.select { |o| user_signed_in? ? o[:favorite] : true }
|
|
37
|
+
%>
|
|
38
|
+
|
|
39
|
+
<%%# Always render the container so model-picker can append a transient
|
|
40
|
+
quick-pick when the user picks an off-row model from the grid. %>
|
|
41
|
+
<div class="quick-picks" data-<%%= stimulus_controller %>-target="quickPicks">
|
|
42
|
+
<%% if default_option %>
|
|
43
|
+
<button type="button"
|
|
44
|
+
class="quick-pick-button is-default"
|
|
45
|
+
title="Your default model"
|
|
46
|
+
data-family="<%%= default_option[:family] %>"
|
|
47
|
+
data-api-key-uuid="<%%= default_option[:api_key_uuid] %>"
|
|
48
|
+
data-model="<%%= default_option[:meta_id] %>"
|
|
49
|
+
data-supports-vision="<%%= default_option[:supports_vision] %>"
|
|
50
|
+
data-action="click-><%%= stimulus_controller %>#pick">
|
|
51
|
+
<%%= default_option[:label] %>
|
|
52
|
+
</button>
|
|
53
|
+
<%% end %>
|
|
54
|
+
|
|
55
|
+
<%% favorite_options.each do |o| %>
|
|
56
|
+
<button type="button"
|
|
57
|
+
class="quick-pick-button"
|
|
58
|
+
title="<%%= o[:meta_id] %>"
|
|
59
|
+
data-family="<%%= o[:family] %>"
|
|
60
|
+
data-api-key-uuid="<%%= o[:api_key_uuid] %>"
|
|
61
|
+
data-model="<%%= o[:meta_id] %>"
|
|
62
|
+
data-supports-vision="<%%= o[:supports_vision] %>"
|
|
63
|
+
data-action="click-><%%= stimulus_controller %>#pick">
|
|
64
|
+
<%%= o[:label] %>
|
|
65
|
+
</button>
|
|
66
|
+
<%% end %>
|
|
67
|
+
</div>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<i class="bi bi-tools"></i>
|
|
11
11
|
Tools
|
|
12
12
|
<span class="tool-count-badge" data-<%%= stimulus_controller %>-target="countBadge" style="display: none;">0</span>
|
|
13
|
-
<i class="bi bi-chevron-
|
|
13
|
+
<i class="bi bi-chevron-up toggle-icon" data-<%%= stimulus_controller %>-target="toggleIcon"></i>
|
|
14
14
|
</button>
|
|
15
15
|
</div>
|
|
16
16
|
<div class="tool-selector-panel" data-<%%= stimulus_controller %>-target="panel" style="display: none;">
|
|
@@ -2,5 +2,23 @@ module LlmMetaClient
|
|
|
2
2
|
module Helpers
|
|
3
3
|
include PromptNavigator::Helpers
|
|
4
4
|
include ChatManager::Helpers
|
|
5
|
+
|
|
6
|
+
# Pull a leading `` image off a user prompt
|
|
7
|
+
# so it can be rendered as a plain <img> while the remaining text stays
|
|
8
|
+
# plain. Returns [img_html_safe_or_nil, remaining_text]. Emits the tag
|
|
9
|
+
# directly (no markdown helper required) so this works in any host.
|
|
10
|
+
ATTACHED_IMAGE_HEAD = /\A!\[[^\]]*\]\(data:([^;]+);base64,([^\)]+)\)\s*\n*/m
|
|
11
|
+
|
|
12
|
+
def split_attached_image_html(text)
|
|
13
|
+
s = text.to_s
|
|
14
|
+
m = s.match(ATTACHED_IMAGE_HEAD)
|
|
15
|
+
return [ nil, s ] unless m
|
|
16
|
+
img = tag.img(
|
|
17
|
+
src: "data:#{m[1]};base64,#{m[2]}",
|
|
18
|
+
alt: "",
|
|
19
|
+
class: "user-attached-image"
|
|
20
|
+
)
|
|
21
|
+
[ img, s.sub(ATTACHED_IMAGE_HEAD, "") ]
|
|
22
|
+
end
|
|
5
23
|
end
|
|
6
24
|
end
|
|
@@ -12,13 +12,25 @@ module LlmMetaClient
|
|
|
12
12
|
# Returns the final assistant content. If tool calls fired, the returned
|
|
13
13
|
# string mirrors the synchronous #call format (response + markdown
|
|
14
14
|
# "Tool calls" section appended) so persistence stays consistent.
|
|
15
|
-
def stream(id_token, api_key_uuid, model_id, context, user_content, tool_ids: [], generation_settings: {})
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
def stream(id_token, api_key_uuid, model_id, context, user_content, tool_ids: [], generation_settings: {}, image_context: nil, image: nil, images: nil)
|
|
16
|
+
if image_context.present?
|
|
17
|
+
prompt_text = user_content.is_a?(Hash) ? (user_content[:prompt] || user_content["prompt"]).to_s : user_content.to_s
|
|
18
|
+
debug_log "Streaming image request to LLM: \n===>\n#{prompt_text}\n(with #{image_context.size} prior turn(s))\n===>"
|
|
19
|
+
body = { prompt: prompt_text, image_context: image_context }
|
|
20
|
+
else
|
|
21
|
+
context_and_user_content = "Context:#{context}, User Prompt: #{user_content}"
|
|
22
|
+
debug_log "Streaming request to LLM: \n===>\n#{context_and_user_content}\n===>"
|
|
23
|
+
body = { prompt: context_and_user_content }
|
|
24
|
+
end
|
|
20
25
|
body[:tool_ids] = tool_ids if tool_ids.present?
|
|
21
26
|
body[:generation_settings] = generation_settings if generation_settings.present?
|
|
27
|
+
# images: ordered chronologically with the current turn's image last.
|
|
28
|
+
# Legacy single `image:` is forwarded as a fallback for older callers.
|
|
29
|
+
if images.present?
|
|
30
|
+
body[:images] = images
|
|
31
|
+
elsif image.present?
|
|
32
|
+
body[:image] = image
|
|
33
|
+
end
|
|
22
34
|
|
|
23
35
|
assembled = +""
|
|
24
36
|
collected_tool_calls = []
|
|
@@ -30,6 +42,11 @@ module LlmMetaClient
|
|
|
30
42
|
when "tool_calls"
|
|
31
43
|
collected_tool_calls = event[:data]["tool_calls"] || []
|
|
32
44
|
yield event if block_given?
|
|
45
|
+
when "thinking"
|
|
46
|
+
# Thinking-mode deltas (Ollama hybrid models): forwarded to the
|
|
47
|
+
# caller for live rendering, but NOT folded into `assembled` —
|
|
48
|
+
# only the final content is persisted as the assistant message.
|
|
49
|
+
yield event if block_given?
|
|
33
50
|
when "done"
|
|
34
51
|
# End-of-stream marker from upstream; no-op here.
|
|
35
52
|
when "error"
|
|
@@ -138,7 +155,7 @@ module LlmMetaClient
|
|
|
138
155
|
|
|
139
156
|
buffer = +""
|
|
140
157
|
response.read_body do |chunk|
|
|
141
|
-
buffer << chunk
|
|
158
|
+
buffer << chunk.force_encoding("UTF-8")
|
|
142
159
|
while (boundary = buffer.index("\n\n"))
|
|
143
160
|
raw_event = buffer.slice!(0, boundary + 2)
|
|
144
161
|
parsed = parse_sse_event(raw_event)
|
|
@@ -173,6 +190,7 @@ module LlmMetaClient
|
|
|
173
190
|
if body.is_a?(Hash)
|
|
174
191
|
err = body["error"]
|
|
175
192
|
msg = body["message"]
|
|
193
|
+
return "Your sign-in expired. Please sign in again." if err.to_s.match?(/token has expired/i)
|
|
176
194
|
return "#{err}: #{msg}" if err.present? && msg.present?
|
|
177
195
|
return err if err.present?
|
|
178
196
|
return msg if msg.present?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm_meta_client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dhq_boiler
|
|
@@ -112,11 +112,16 @@ files:
|
|
|
112
112
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/chat_streams_controller.rb
|
|
113
113
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb
|
|
114
114
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/prompts_controller.rb
|
|
115
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/asset_actions_controller.js
|
|
116
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_controller.js
|
|
117
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_menu_controller.js
|
|
115
118
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_title_edit_controller.js
|
|
116
119
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chats_form_controller.js
|
|
117
120
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/generation_settings_controller.js
|
|
118
|
-
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/
|
|
121
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/input_controls_controller.js
|
|
122
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/llm_toggle_controller.js
|
|
119
123
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/message_stream_controller.js
|
|
124
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/model_picker_controller.js
|
|
120
125
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/tool_selector_controller.js
|
|
121
126
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/popover.js
|
|
122
127
|
- lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb
|
|
@@ -127,16 +132,16 @@ files:
|
|
|
127
132
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_streaming_message.html.erb
|
|
128
133
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_tool_call_message.html.erb
|
|
129
134
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb
|
|
135
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/destroy.turbo_stream.erb
|
|
130
136
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/edit.html.erb
|
|
131
137
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/new.html.erb
|
|
132
|
-
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/update.turbo_stream.erb
|
|
133
138
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/_header.html.erb
|
|
139
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/_new_chat_button.html.erb
|
|
134
140
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/_sidebar.html.erb
|
|
135
141
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/application.html.erb
|
|
136
|
-
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_api_key_field.html.erb
|
|
137
|
-
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_family_field.html.erb
|
|
138
142
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_generation_settings_field.html.erb
|
|
139
|
-
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/
|
|
143
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_model_grid.html.erb
|
|
144
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_quick_picks.html.erb
|
|
140
145
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_tool_selector_field.html.erb
|
|
141
146
|
- lib/generators/llm_meta_client/scaffold/templates/config/initializers/llm_service.rb
|
|
142
147
|
- lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_chats.rb
|