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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/lib/generators/llm_meta_client/scaffold/scaffold_generator.rb +12 -7
  4. data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/api/mcp_servers_controller.rb +2 -2
  5. data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chat_streams_controller.rb +24 -2
  6. data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb +92 -76
  7. data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/prompts_controller.rb +28 -1
  8. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/asset_actions_controller.js +98 -0
  9. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_controller.js +126 -0
  10. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_menu_controller.js +42 -0
  11. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_title_edit_controller.js +5 -0
  12. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chats_form_controller.js +186 -12
  13. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/generation_settings_controller.js +38 -20
  14. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/input_controls_controller.js +55 -0
  15. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/llm_toggle_controller.js +27 -0
  16. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/message_stream_controller.js +102 -3
  17. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/model_picker_controller.js +160 -0
  18. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/tool_selector_controller.js +10 -2
  19. data/lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb +130 -44
  20. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_chat_sidebar.html.erb +3 -1
  21. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_message.html.erb +3 -1
  22. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_streaming_message.html.erb +6 -0
  23. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_tool_call_message.html.erb +20 -18
  24. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb +31 -0
  25. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/destroy.turbo_stream.erb +3 -0
  26. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/edit.html.erb +53 -17
  27. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/new.html.erb +50 -17
  28. data/lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/_header.html.erb +1 -5
  29. data/lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/_new_chat_button.html.erb +7 -0
  30. data/lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/application.html.erb +2 -2
  31. data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_generation_settings_field.html.erb +7 -5
  32. data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_model_grid.html.erb +88 -0
  33. data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_quick_picks.html.erb +67 -0
  34. data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_tool_selector_field.html.erb +1 -1
  35. data/lib/llm_meta_client/helpers.rb +18 -0
  36. data/lib/llm_meta_client/server_query.rb +24 -6
  37. data/lib/llm_meta_client/version.rb +1 -1
  38. metadata +11 -6
  39. data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/llm_selector_controller.js +0 -236
  40. data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/update.turbo_stream.erb +0 -85
  41. data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_api_key_field.html.erb +0 -15
  42. data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_family_field.html.erb +0 -18
  43. data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_model_field.html.erb +0 -12
@@ -1,236 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus"
2
-
3
- // Connects to data-controller="llm-selector"
4
- export default class extends Controller {
5
- static targets = ["family", "apiKey", "model"]
6
-
7
- connect() {
8
- this.#setDefaults()
9
- this.dispatch("changed")
10
- }
11
-
12
- familyChanged(event) {
13
- const selectedFamily = event.target.value
14
- const familiesData = event.target.dataset.families
15
-
16
- if (!selectedFamily || !familiesData) {
17
- this.#showApiKeyField()
18
- this.#clearApiKeySelect()
19
- this.#clearModelSelect()
20
- return
21
- }
22
-
23
- try {
24
- const families = JSON.parse(familiesData)
25
- const family = families.find((f) => f.llm_type === selectedFamily)
26
-
27
- if (family?.api_keys) {
28
- if (selectedFamily === "ollama") {
29
- // Ollama: skip API key selection entirely, go straight to model
30
- const apiKey = family.api_keys[0]
31
- this.#hideApiKeyField()
32
- this.apiKeyTarget.disabled = false
33
- this.apiKeyTarget.innerHTML =
34
- `<option value="${apiKey.uuid}" selected>${apiKey.description}</option>`
35
- if (apiKey.available_models) {
36
- this.#populateModelSelect(apiKey.available_models)
37
- } else {
38
- this.#clearModelSelect()
39
- }
40
- } else {
41
- this.#showApiKeyField()
42
- this.#populateApiKeySelect(family.api_keys)
43
- }
44
- } else {
45
- this.#showApiKeyField()
46
- this.#clearApiKeySelect()
47
- this.#clearModelSelect()
48
- }
49
- } catch (e) {
50
- console.error("Failed to parse families data:", e)
51
- this.#clearApiKeySelect()
52
- this.#clearModelSelect()
53
- }
54
- }
55
-
56
- apiKeyChanged(event) {
57
- const selectedValue = event.target.value
58
- const familiesData = this.hasFamilyTarget
59
- ? this.familyTarget.dataset.families
60
- : null
61
- const selectedFamily = this.hasFamilyTarget
62
- ? this.familyTarget.value
63
- : null
64
-
65
- if (!selectedValue || !familiesData || !selectedFamily) {
66
- this.#clearModelSelect()
67
- return
68
- }
69
-
70
- try {
71
- const families = JSON.parse(familiesData)
72
- const family = families.find((f) => f.llm_type === selectedFamily)
73
- const selectedKey = family?.api_keys?.find(
74
- (k) => k.uuid === selectedValue
75
- )
76
-
77
- if (selectedKey?.available_models) {
78
- this.#populateModelSelect(selectedKey.available_models)
79
- } else {
80
- this.#clearModelSelect()
81
- }
82
- } catch (e) {
83
- console.error("Failed to parse families data:", e)
84
- this.#clearModelSelect()
85
- }
86
- }
87
-
88
- #setDefaults() {
89
- const urlParams = new URLSearchParams(window.location.search)
90
- const defaultFamily = urlParams.get("family")
91
- const defaultApiKey = urlParams.get("api_key_uuid")
92
- const defaultModel = urlParams.get("model")
93
-
94
- if (defaultFamily && this.hasFamilyTarget) {
95
- const familyOption = Array.from(this.familyTarget.options).find(
96
- (o) => o.value === defaultFamily
97
- )
98
- if (familyOption) {
99
- this.familyTarget.value = familyOption.value
100
- this.familyChanged({ target: this.familyTarget })
101
-
102
- if (defaultFamily === "ollama") {
103
- // Ollama: API key is auto-selected by familyChanged, just set model
104
- if (defaultModel && this.hasModelTarget) {
105
- const modelOption = Array.from(this.modelTarget.options).find(
106
- (o) => o.value === defaultModel
107
- )
108
- if (modelOption) {
109
- this.modelTarget.value = modelOption.value
110
- }
111
- }
112
- } else if (defaultApiKey && this.hasApiKeyTarget) {
113
- const apiKeyOption = Array.from(this.apiKeyTarget.options).find(
114
- (o) => o.value === defaultApiKey
115
- )
116
- if (apiKeyOption) {
117
- this.apiKeyTarget.value = apiKeyOption.value
118
- this.apiKeyChanged({ target: this.apiKeyTarget })
119
-
120
- if (defaultModel && this.hasModelTarget) {
121
- const modelOption = Array.from(this.modelTarget.options).find(
122
- (o) => o.value === defaultModel
123
- )
124
- if (modelOption) {
125
- this.modelTarget.value = modelOption.value
126
- }
127
- }
128
- }
129
- }
130
- }
131
- } else if (defaultApiKey && this.hasFamilyTarget) {
132
- // Fallback: try to find the family from the API key UUID
133
- this.#setDefaultsFromApiKey(defaultApiKey, defaultModel)
134
- }
135
- }
136
-
137
- #setDefaultsFromApiKey(apiKeyUuid, defaultModel) {
138
- const familiesData = this.hasFamilyTarget
139
- ? this.familyTarget.dataset.families
140
- : null
141
- if (!familiesData) return
142
-
143
- try {
144
- const families = JSON.parse(familiesData)
145
- for (const family of families) {
146
- const key = family.api_keys?.find((k) => k.uuid === apiKeyUuid)
147
- if (key) {
148
- this.familyTarget.value = family.llm_type
149
- this.familyChanged({ target: this.familyTarget })
150
-
151
- if (family.llm_type !== "ollama") {
152
- this.apiKeyTarget.value = apiKeyUuid
153
- this.apiKeyChanged({ target: this.apiKeyTarget })
154
- }
155
-
156
- if (defaultModel && this.hasModelTarget) {
157
- const modelOption = Array.from(this.modelTarget.options).find(
158
- (o) => o.value === defaultModel
159
- )
160
- if (modelOption) {
161
- this.modelTarget.value = modelOption.value
162
- }
163
- }
164
- break
165
- }
166
- }
167
- } catch (e) {
168
- console.error("Failed to set defaults from API key:", e)
169
- }
170
- }
171
-
172
- #populateApiKeySelect(apiKeys) {
173
- if (!this.hasApiKeyTarget) return
174
-
175
- this.apiKeyTarget.innerHTML =
176
- '<option value="">Please select a service</option>'
177
- this.apiKeyTarget.disabled = false
178
-
179
- for (const key of apiKeys) {
180
- const option = document.createElement("option")
181
- option.value = key.uuid
182
- option.textContent = key.description
183
- this.apiKeyTarget.appendChild(option)
184
- }
185
-
186
- // Clear model when API key list changes
187
- this.#clearModelSelect()
188
- this.dispatch("changed")
189
- }
190
-
191
- #clearApiKeySelect() {
192
- if (!this.hasApiKeyTarget) return
193
-
194
- this.apiKeyTarget.innerHTML =
195
- '<option value="">Please select a family first</option>'
196
- this.apiKeyTarget.disabled = true
197
- this.#clearModelSelect()
198
- this.dispatch("changed")
199
- }
200
-
201
- #hideApiKeyField() {
202
- if (!this.hasApiKeyTarget) return
203
- this.apiKeyTarget.closest(".api-key-field").classList.add("hidden")
204
- }
205
-
206
- #showApiKeyField() {
207
- if (!this.hasApiKeyTarget) return
208
- this.apiKeyTarget.closest(".api-key-field").classList.remove("hidden")
209
- }
210
-
211
- #populateModelSelect(models) {
212
- if (!this.hasModelTarget) return
213
-
214
- this.modelTarget.innerHTML =
215
- '<option value="">Please select a model</option>'
216
- this.modelTarget.disabled = false
217
-
218
- for (const model of models) {
219
- const option = document.createElement("option")
220
- option.value = model.value
221
- option.textContent = model.label
222
- this.modelTarget.appendChild(option)
223
- }
224
-
225
- this.dispatch("changed")
226
- }
227
-
228
- #clearModelSelect() {
229
- if (!this.hasModelTarget) return
230
-
231
- this.modelTarget.innerHTML =
232
- '<option value="">Please select a service first</option>'
233
- this.modelTarget.disabled = true
234
- this.dispatch("changed")
235
- }
236
- }
@@ -1,85 +0,0 @@
1
- <%% # Clear the message input %>
2
- <%%= turbo_stream.update "message-input" do %>
3
- <%% end %>
4
-
5
- <%% # User message is already shown by JavaScript on form submit %>
6
- <%% # Only render assistant message here %>
7
-
8
- <%%# Render streaming assistant placeholder; the message-stream Stimulus controller opens an EventSource and appends deltas as they arrive. %>
9
- <%% if @prompt_execution && @error_message.blank? %>
10
- <%%= turbo_stream.append "messages-list" do %>
11
- <%%= render partial: "chats/streaming_message", locals: { chat: @chat, prompt_execution: @prompt_execution } %>
12
- <%% end %>
13
- <%% end %>
14
-
15
- <%% # Show error message if any %>
16
- <%% if @error_message %>
17
- <%%= turbo_stream.append "messages-list" do %>
18
- <div class="message error">
19
- <div class="message-content">
20
- <p><%%= @error_message %></p>
21
- </div>
22
- </div>
23
- <%% end %>
24
- <%% end %>
25
-
26
- <%% # Update history sidebar - replace entire content to ensure update %>
27
- <%% if @prompt_execution %>
28
- <%%= turbo_stream.replace "history-sidebar" do %>
29
- <div id="history-sidebar">
30
- <h2>History</h2>
31
- <div class="history-stack" id="history-stack" data-controller="history">
32
- <%%= render 'prompt_navigator/history_card', locals: {
33
- ann: @prompt_execution,
34
- next_ann: (@chat&.ordered_by_descending_prompt_executions || [])[1],
35
- is_active: @prompt_execution.execution_id == @active_message_uuid,
36
- card_path: ->(uuid) { prompt_path(uuid) }
37
- } %>
38
- <%% (@chat&.ordered_by_descending_prompt_executions || []).drop(1).each_with_index do |ann, idx| %>
39
- <%%= render 'prompt_navigator/history_card', locals: {
40
- ann: ann,
41
- next_ann: @chat.ordered_by_descending_prompt_executions[idx + 2],
42
- is_active: ann.execution_id == @active_message_uuid,
43
- card_path: ->(uuid) { prompt_path(uuid) }
44
- } %>
45
- <%% end %>
46
- <svg class="history-arrows" data-history-target="svg"></svg>
47
- </div>
48
- </div>
49
- <%% end %>
50
- <%% end %>
51
-
52
- <%% # Clear input, refocus, and scroll %>
53
- <turbo-stream action="after" target="messages-list">
54
- <template>
55
- <script>
56
- (function() {
57
- // Clear and refocus message input
58
- const messageInput = document.getElementById('message-input');
59
- if (messageInput) {
60
- messageInput.value = '';
61
- messageInput.focus();
62
- }
63
-
64
- // Update submit button state
65
- const form = document.querySelector('[data-controller="chats-form"]');
66
- if (form && messageInput) {
67
- const event = new Event('input', { bubbles: true });
68
- messageInput.dispatchEvent(event);
69
- }
70
-
71
- // Update branch_from_uuid to the latest prompt execution
72
- const branchField = document.getElementById('branch_from_uuid');
73
- if (branchField) {
74
- branchField.value = '<%%= @prompt_execution&.execution_id %>';
75
- }
76
-
77
- // Scroll to bottom
78
- const chatMessages = document.getElementById('chat-messages');
79
- if (chatMessages) {
80
- chatMessages.scrollTop = chatMessages.scrollHeight;
81
- }
82
- })();
83
- </script>
84
- </template>
85
- </turbo-stream>
@@ -1,15 +0,0 @@
1
- <%%
2
- stimulus_controller = local_assigns[:stimulus_controller]
3
- # Convert hyphens to underscores for Stimulus targets on the Rails side
4
- stimulus_target_prefix = stimulus_controller.gsub('-', '_')
5
- %>
6
- <div class="api-key-field">
7
- <label>LLM Service</label>
8
- <%%= select_tag :api_key_uuid,
9
- options_for_select([["Please select a family first", ""]]),
10
- { required: true, disabled: true,
11
- data: {
12
- "#{stimulus_target_prefix}-target": "apiKey",
13
- action: "change->#{stimulus_controller}#apiKeyChanged"
14
- } } %>
15
- </div>
@@ -1,18 +0,0 @@
1
- <%%
2
- llm_families = local_assigns[:llm_families] || []
3
- stimulus_controller = local_assigns[:stimulus_controller]
4
- # Convert hyphens to underscores for Stimulus targets on the Rails side
5
- stimulus_target_prefix = stimulus_controller.gsub('-', '_')
6
- %>
7
- <div class="family-field">
8
- <label>Family</label>
9
- <%%= select_tag :family,
10
- options_for_select([["Please select a family", ""]] +
11
- llm_families.map { |f| [f[:name], f[:llm_type]] }),
12
- { required: true,
13
- data: {
14
- "#{stimulus_target_prefix}-target": "family",
15
- action: "change->#{stimulus_controller}#familyChanged",
16
- families: llm_families.to_json
17
- } } %>
18
- </div>
@@ -1,12 +0,0 @@
1
- <%%
2
- stimulus_controller = local_assigns[:stimulus_controller]
3
- # Convert hyphens to underscores for Stimulus targets on the Rails side
4
- stimulus_target_prefix = stimulus_controller.gsub('-', '_')
5
- %>
6
-
7
- <div class="model-field">
8
- <label>LLM model</label>
9
- <%%= select_tag :model, options_for_select([["Please select a service first", ""]]),
10
- { required: true, disabled: true,
11
- data: { "#{stimulus_target_prefix}-target": "model" } } %>
12
- </div>