llm_meta_client 0.6.1 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 931f6b078e6df954f5ab32218cb31c4c743112ed6ae037c3e027102cec3089d4
4
- data.tar.gz: 52febe0e221d964ad286ce8ad7ebb0ad595a8d80b87a4afa55af54e82e82873d
3
+ metadata.gz: 95bb1f421088cc0c287b4f79e961455a1a5275b860b372fb23523ddc8781d37a
4
+ data.tar.gz: d4f92a150de0d9996d3ea6c54e69fe24989763f4bd359ce46a6b7ff69d36ecb7
5
5
  SHA512:
6
- metadata.gz: 6216c95f340ae0ef1b3e9ee54bd8a0906c2001d50ac45139df4f8da584c856962b462182e7fcc20e94c8785e6923f50f04882bf534da300bc8946fef5308cfa1
7
- data.tar.gz: 2223f1a7381b9bdb484a1ec8fade18c5b83789b007b6f078680887a81db4c6a5854572310a108864eba89656cff980359b9602e14a9cdae752d8d0ad98d7a48e
6
+ metadata.gz: 480919bad702190fec4166d8a2659121ff62b1880123e6db168d9a1c8a8b7f80f23ca175e2e6f4a31b96e6bde97fd0723d95caa71439845a5017c351c18e373e
7
+ data.tar.gz: c2600201eccae124c9929eabcd8b7d755b7dbac4092cd5441c55d1caf79c839af8f0b41b49d7bf1c2e6a324e33c6bd1957d29a1ac3e837a7ab6924d727e8ae91
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.0] - 2026-03-25
9
+
10
+ ### Changed
11
+
12
+ - Replace generation settings UI from individual sliders to JSON textarea input
13
+ - Improve prompt execution branching logic to use `execution_id` instead of message UUID
14
+ - Use `find_by!` for proper 404 handling in controllers
15
+ - Use URL-based chat lookup in `chats#update` instead of session-based lookup
16
+ - Keep existing chat when switching model or LLM (update instead of creating new chat)
17
+ - Upgrade `prompt_navigator` dependency to `~> 1.0`
18
+ - Upgrade `chat_manager` dependency to `~> 1.0`
19
+
20
+ ### Fixed
21
+
22
+ - Fix Turbo Stream history sidebar element ID mismatch (`history-content` → `history-sidebar`)
23
+ - Fix `next_ann` for proper history card rendering
24
+ - Wrap inline JavaScript in IIFE to prevent variable conflicts across Turbo Stream updates
25
+ - Fix scroll event listener duplication across Turbo navigations
26
+ - Validate generation settings JSON input before sending to LLM
27
+
8
28
  ## [0.6.1] - 2026-03-19
9
29
 
10
30
  ### Fixed
@@ -31,60 +31,39 @@
31
31
  padding: 12px 16px;
32
32
  }
33
33
 
34
- .generation-setting-item {
35
- margin-bottom: 14px;
36
-
37
- &:last-child {
38
- margin-bottom: 0;
39
- }
34
+ .generation-settings-label {
35
+ display: block;
36
+ font-size: 13px;
37
+ font-weight: 600;
38
+ color: #374151;
39
+ margin-bottom: 6px;
40
+ }
40
41
 
41
- label {
42
- display: flex;
43
- align-items: center;
44
- gap: 8px;
45
- font-size: 13px;
46
- font-weight: 600;
47
- color: #374151;
48
- margin-bottom: 4px;
49
- }
42
+ .generation-settings-json-input {
43
+ width: 100%;
44
+ padding: 8px 10px;
45
+ border: 1px solid #d1d5db;
46
+ border-radius: 6px;
47
+ font-size: 13px;
48
+ font-family: "SFMono-Regular", "Consolas", "Liberation Mono", "Menlo", monospace;
49
+ background-color: white;
50
+ resize: vertical;
51
+ transition: border-color 0.2s;
52
+ box-sizing: border-box;
50
53
 
51
- input[type="range"] {
52
- width: 100%;
53
- accent-color: #3b82f6;
54
- cursor: pointer;
54
+ &:focus {
55
+ outline: none;
56
+ border-color: #3b82f6;
57
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
55
58
  }
56
59
 
57
- .max-tokens-input {
58
- width: 100%;
59
- padding: 6px 10px;
60
- border: 1px solid #d1d5db;
61
- border-radius: 6px;
62
- font-size: 13px;
63
- background-color: white;
64
- transition: border-color 0.2s;
65
-
66
- &:focus {
67
- outline: none;
68
- border-color: #3b82f6;
69
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
70
- }
71
-
72
- &::placeholder {
73
- color: #9ca3af;
74
- }
60
+ &::placeholder {
61
+ color: #9ca3af;
75
62
  }
76
63
  }
77
64
 
78
- .setting-value {
79
- font-weight: 700;
80
- color: #3b82f6;
81
- font-size: 13px;
82
- }
83
-
84
- .setting-range-labels {
85
- display: flex;
86
- justify-content: space-between;
65
+ .generation-settings-hint {
87
66
  font-size: 11px;
88
67
  color: #9ca3af;
89
- margin-top: 2px;
68
+ margin-top: 4px;
90
69
  }
@@ -56,6 +56,7 @@ module LlmMetaClient
56
56
  def add_migrations
57
57
  migration_template "db/migrate/create_chats.rb", "db/migrate/create_chats.rb"
58
58
  migration_template "db/migrate/create_messages.rb", "db/migrate/create_messages.rb"
59
+ migration_template "db/migrate/migrate_llm_uuid_to_prompt_executions.rb", "db/migrate/migrate_llm_uuid_to_prompt_executions.rb"
59
60
  end
60
61
 
61
62
  def configure_routes
@@ -10,7 +10,8 @@ class ChatsController < ApplicationController
10
10
  # Initialize chat context
11
11
  initialize_chat current_user&.chats
12
12
 
13
- @chat = current_user&.chats.includes(:messages).find_by(uuid: params[:id])
13
+ @chat = current_user&.chats.includes(:messages).find_by!(uuid: params[:id])
14
+ session[:chat_id] = @chat.id
14
15
  @messages = @chat.ordered_messages
15
16
 
16
17
  # Initialize history
@@ -60,9 +61,7 @@ class ChatsController < ApplicationController
60
61
  # Find or create chat
61
62
  @chat = Chat.find_or_switch_for_session(
62
63
  session,
63
- current_user,
64
- llm_uuid: params[:api_key_uuid],
65
- model: params[:model]
64
+ current_user
66
65
  )
67
66
  add_chat @chat
68
67
  @messages = @chat&.ordered_messages || []
@@ -71,8 +70,21 @@ class ChatsController < ApplicationController
71
70
  initialize_history @chat&.ordered_by_descending_prompt_executions
72
71
 
73
72
  if params[:message].present?
73
+ # Validate generation settings before proceeding
74
+ begin
75
+ generation_settings = generation_settings_param
76
+ rescue InvalidGenerationSettingsError => e
77
+ @error_message = e.message
78
+ respond_to do |format|
79
+ format.turbo_stream
80
+ format.html { redirect_to new_chat_path, alert: e.message }
81
+ end
82
+ return
83
+ end
84
+
74
85
  # Add user message (will be rendered via turbo stream)
75
86
  @prompt_execution, @user_message = @chat.add_user_message(params[:message],
87
+ params[:api_key_uuid],
76
88
  params[:model],
77
89
  params[:branch_from_uuid])
78
90
  # Push to history for rendering
@@ -82,7 +94,7 @@ class ChatsController < ApplicationController
82
94
 
83
95
  # Send to LLM and get assistant response
84
96
  begin
85
- @assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings: generation_settings_param)
97
+ @assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings: generation_settings)
86
98
  # Generate chat title from the user's prompt (only if title is not yet set)
87
99
  @chat.generate_title(params[:message], jwt_token)
88
100
  rescue StandardError => e
@@ -131,9 +143,7 @@ class ChatsController < ApplicationController
131
143
 
132
144
  @chat = Chat.find_or_switch_for_session(
133
145
  session,
134
- current_user,
135
- llm_uuid: params[:api_key_uuid],
136
- model: params[:model]
146
+ current_user
137
147
  )
138
148
  @messages = @chat&.ordered_messages || []
139
149
  # initialize history for the chat
@@ -149,20 +159,29 @@ class ChatsController < ApplicationController
149
159
  def update
150
160
  jwt_token = current_user.id_token if user_signed_in?
151
161
 
152
- # Find or create chat
153
- @chat = Chat.find_or_switch_for_session(
154
- session,
155
- current_user,
156
- llm_uuid: params[:api_key_uuid],
157
- model: params[:model]
158
- )
162
+ # Use the chat identified by the URL, not the session
163
+ @chat = current_user.chats.find(params[:id])
164
+ session[:chat_id] = @chat.id
159
165
  @messages = @chat&.ordered_messages || []
160
166
  # initialize history for the chat
161
167
  initialize_history @chat&.ordered_by_descending_prompt_executions
162
168
 
163
169
  if params[:message].present?
170
+ # Validate generation settings before proceeding
171
+ begin
172
+ generation_settings = generation_settings_param
173
+ rescue InvalidGenerationSettingsError => e
174
+ @error_message = e.message
175
+ respond_to do |format|
176
+ format.turbo_stream
177
+ format.html { redirect_to chat_path(@chat), alert: e.message }
178
+ end
179
+ return
180
+ end
181
+
164
182
  # Add user message (will be rendered via turbo stream)
165
183
  @prompt_execution, @user_message = @chat.add_user_message(params[:message],
184
+ params[:api_key_uuid],
166
185
  params[:model],
167
186
  params[:branch_from_uuid])
168
187
  # Push to history for rendering
@@ -172,7 +191,7 @@ class ChatsController < ApplicationController
172
191
 
173
192
  # Send to LLM and get assistant response
174
193
  begin
175
- @assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings: generation_settings_param)
194
+ @assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings: generation_settings)
176
195
  rescue StandardError => e
177
196
  Rails.logger.error "Error in chat response: #{e.class} - #{e.message}\n#{e.backtrace&.join("\n")}"
178
197
  @error_message = "An error occurred while getting the response. Please try again."
@@ -192,13 +211,27 @@ class ChatsController < ApplicationController
192
211
  params[:tool_ids].presence || []
193
212
  end
194
213
 
214
+ ALLOWED_GENERATION_KEYS = %w[temperature top_k top_p max_tokens repeat_penalty].freeze
215
+
216
+ class InvalidGenerationSettingsError < StandardError; end
217
+
195
218
  def generation_settings_param
196
- settings = {}
197
- settings[:temperature] = params[:temperature].to_f if params[:temperature].present?
198
- settings[:top_k] = params[:top_k].to_i if params[:top_k].present?
199
- settings[:top_p] = params[:top_p].to_f if params[:top_p].present?
200
- settings[:max_tokens] = params[:max_tokens].to_i if params[:max_tokens].present?
201
- settings[:repeat_penalty] = params[:repeat_penalty].to_f if params[:repeat_penalty].present?
202
- settings
219
+ return {} if params[:generation_settings_json].blank?
220
+
221
+ parsed = JSON.parse(params[:generation_settings_json])
222
+ raise InvalidGenerationSettingsError, "Generation settings must be a JSON object" unless parsed.is_a?(Hash)
223
+
224
+ settings = parsed.slice(*ALLOWED_GENERATION_KEYS)
225
+ invalid_keys = parsed.keys - ALLOWED_GENERATION_KEYS
226
+ raise InvalidGenerationSettingsError, "Unknown keys: #{invalid_keys.join(', ')}" if invalid_keys.any?
227
+
228
+ non_numeric = settings.reject { |_k, v| v.is_a?(Numeric) }
229
+ if non_numeric.any?
230
+ raise InvalidGenerationSettingsError, "Values must be numeric: #{non_numeric.keys.join(', ')}"
231
+ end
232
+
233
+ settings.symbolize_keys
234
+ rescue JSON::ParserError => e
235
+ raise InvalidGenerationSettingsError, "Invalid JSON: #{e.message}"
203
236
  end
204
237
  end
@@ -4,8 +4,8 @@ class PromptsController < ApplicationController
4
4
  skip_before_action :authenticate_user!, raise: false
5
5
 
6
6
  def show
7
- @prompt_execution = PromptNavigator::PromptExecution.includes(:messages).find_by(execution_id: params[:id])
8
- @message = @prompt_execution.messages.first
7
+ @prompt_execution = PromptNavigator::PromptExecution.find_by!(execution_id: params[:id])
8
+ @message = Message.where(prompt_navigator_prompt_execution: @prompt_execution).order(:created_at).first
9
9
  @chat = @message.chat
10
10
  @messages = @chat.ordered_messages
11
11
 
@@ -25,6 +25,9 @@ class PromptsController < ApplicationController
25
25
  # Set active UUID for history sidebar highlighting
26
26
  set_active_message_uuid(@prompt_execution.execution_id)
27
27
 
28
+ # Set branch_from_uuid so the form knows which message to branch from
29
+ @branch_from_uuid = @prompt_execution.execution_id
30
+
28
31
  render "chats/edit"
29
32
  rescue StandardError => e
30
33
  Rails.logger.error "Error in PromptsController#show_by_uuid: #{e.class} - #{e.message}\n#{e.backtrace&.join("\n")}"
@@ -6,15 +6,7 @@ export default class extends Controller {
6
6
  "toggleButton",
7
7
  "toggleIcon",
8
8
  "panel",
9
- "temperatureRange",
10
- "temperatureValue",
11
- "topKRange",
12
- "topKValue",
13
- "topPRange",
14
- "topPValue",
15
- "maxTokensInput",
16
- "repeatPenaltyRange",
17
- "repeatPenaltyValue",
9
+ "jsonInput",
18
10
  ]
19
11
 
20
12
  connect() {
@@ -32,20 +24,4 @@ export default class extends Controller {
32
24
  this.toggleIconTarget.classList.toggle("bi-chevron-up", this.expanded)
33
25
  }
34
26
  }
35
-
36
- updateTemperature() {
37
- this.temperatureValueTarget.textContent = this.temperatureRangeTarget.value
38
- }
39
-
40
- updateTopK() {
41
- this.topKValueTarget.textContent = this.topKRangeTarget.value
42
- }
43
-
44
- updateTopP() {
45
- this.topPValueTarget.textContent = this.topPRangeTarget.value
46
- }
47
-
48
- updateRepeatPenalty() {
49
- this.repeatPenaltyValueTarget.textContent = this.repeatPenaltyRangeTarget.value
50
- }
51
27
  }
@@ -6,25 +6,14 @@ class Chat < ApplicationRecord
6
6
 
7
7
  before_create :set_uuid
8
8
 
9
- validates :llm_uuid, presence: true
10
- validates :model, presence: true
11
-
12
9
  # Find existing chat from session or create new one
13
10
  class << self
14
- def find_or_switch_for_session(session, current_user, llm_uuid: nil, model: nil)
11
+ def find_or_switch_for_session(session, current_user)
15
12
  chat = find_by_session_chat_id(session, current_user)
16
- return chat if llm_uuid.nil? || model.nil?
17
-
18
- # Create new chat if it doesn't exist or LLM/model has changed
19
- if llm_uuid.present? && model.present? && (chat.nil? || (chat.present? && chat.needs_reset?(llm_uuid, model)))
20
- chat = create!(
21
- user: current_user,
22
- llm_uuid: llm_uuid,
23
- model: model
24
- )
25
- session[:chat_id] = chat.id
26
- end
13
+ return chat if chat.present?
27
14
 
15
+ chat = create!(user: current_user)
16
+ session[:chat_id] = chat.id
28
17
  chat
29
18
  end
30
19
 
@@ -41,21 +30,19 @@ class Chat < ApplicationRecord
41
30
  end
42
31
  end
43
32
 
44
- # Get the LLM type for this chat
45
- def llm_type(jwt_token)
46
- llm_options = LlmMetaClient::ServerResource.available_llm_options(jwt_token)
47
- selected_llm = llm_options.find { |opt| opt[:uuid] == llm_uuid }
48
- selected_llm&.dig(:llm_type) || "unknown"
49
- end
50
-
51
33
  # Add a user message to the chat
52
- def add_user_message(message, model, branch_from_uuid = nil)
53
- parent_message = branch_from_uuid.present? ? messages.find_by(uuid: branch_from_uuid) : nil
34
+ def add_user_message(message, llm_uuid, model, branch_from_execution_id = nil)
35
+ previous_id = if branch_from_execution_id.present?
36
+ PromptNavigator::PromptExecution.find_by(execution_id: branch_from_execution_id)&.id
37
+ else
38
+ messages.where(role: "user").order(:created_at).last&.prompt_navigator_prompt_execution_id
39
+ end
54
40
  prompt_execution = PromptNavigator::PromptExecution.create!(
55
41
  prompt: message,
42
+ llm_uuid: llm_uuid,
56
43
  model: model,
57
44
  configuration: "",
58
- previous_id: parent_message&.prompt_navigator_prompt_execution_id
45
+ previous_id: previous_id
59
46
  )
60
47
 
61
48
  new_message = messages.create!(
@@ -68,9 +55,9 @@ class Chat < ApplicationRecord
68
55
 
69
56
  # Add assistant response by sending to LLM
70
57
  def add_assistant_response(prompt_execution, jwt_token, tool_ids: [], generation_settings: {})
71
- response_content = send_to_llm(jwt_token, tool_ids: tool_ids, generation_settings: generation_settings)
58
+ response_content = send_to_llm(prompt_execution, jwt_token, tool_ids: tool_ids, generation_settings: generation_settings)
72
59
  prompt_execution.update!(
73
- llm_platform: llm_type(jwt_token),
60
+ llm_platform: resolve_llm_type(prompt_execution.llm_uuid, jwt_token),
74
61
  response: response_content
75
62
  )
76
63
  new_message = messages.create!(
@@ -98,19 +85,24 @@ class Chat < ApplicationRecord
98
85
  .map(&:prompt_navigator_prompt_execution)
99
86
  end
100
87
 
101
- # Check if chat needs to be reset due to LLM or model change
102
- def needs_reset?(new_llm_uuid, new_model)
103
- llm_uuid != new_llm_uuid || model != new_model
104
- end
105
-
106
88
  private
107
89
 
90
+ # Resolve the LLM type (e.g. "openai", "google") from a given llm_uuid
91
+ def resolve_llm_type(llm_uuid, jwt_token)
92
+ llm_options = LlmMetaClient::ServerResource.available_llm_options(jwt_token)
93
+ selected_llm = llm_options.find { |opt| opt[:uuid] == llm_uuid }
94
+ selected_llm&.dig(:llm_type) || "unknown"
95
+ end
96
+
108
97
  # Summarize the user's prompt into a short title via LLM (required by ChatManager::TitleGeneratable)
109
98
  def summarize_for_title(prompt_text, jwt_token)
99
+ latest_pe = ordered_by_descending_prompt_executions.first
100
+ return nil unless latest_pe&.llm_uuid && latest_pe&.model
101
+
110
102
  LlmMetaClient::ServerQuery.new.call(
111
103
  jwt_token,
112
- llm_uuid,
113
- model,
104
+ latest_pe.llm_uuid,
105
+ latest_pe.model,
114
106
  "No context available.",
115
107
  { role: "user", prompt: "Please summarize the following text into a short title (max 50 characters). Respond with only the title, nothing else: #{prompt_text}" }
116
108
  )
@@ -122,7 +114,10 @@ class Chat < ApplicationRecord
122
114
  end
123
115
 
124
116
  # Send messages to LLM and get response
125
- def send_to_llm(jwt_token, tool_ids: [], generation_settings: {})
117
+ def send_to_llm(prompt_execution, jwt_token, tool_ids: [], generation_settings: {})
118
+ llm_uuid = prompt_execution.llm_uuid
119
+ model = prompt_execution.model
120
+
126
121
  # Get LLM options
127
122
  llm_options = LlmMetaClient::ServerResource.available_llm_options(jwt_token)
128
123
 
@@ -33,12 +33,12 @@
33
33
  <%% # Update history sidebar - replace entire content to ensure update %>
34
34
  <%% if @prompt_execution %>
35
35
  <%%= turbo_stream.replace "history-sidebar" do %>
36
- <div id="history-content">
36
+ <div id="history-sidebar">
37
37
  <h2>History</h2>
38
38
  <div class="history-stack" id="history-stack" data-controller="history">
39
39
  <%%= render 'prompt_navigator/history_card', locals: {
40
40
  ann: @prompt_execution,
41
- next_ann: nil,
41
+ next_ann: (@chat&.ordered_by_descending_prompt_executions || [])[1],
42
42
  is_active: @prompt_execution.execution_id == @active_message_uuid,
43
43
  card_path: ->(uuid) { prompt_path(uuid) }
44
44
  } %>
@@ -60,25 +60,33 @@
60
60
  <turbo-stream action="after" target="messages-list">
61
61
  <template>
62
62
  <script>
63
- // Clear and refocus message input
64
- const messageInput = document.getElementById('message-input');
65
- if (messageInput) {
66
- messageInput.value = '';
67
- messageInput.focus();
68
- }
63
+ (function() {
64
+ // Clear and refocus message input
65
+ const messageInput = document.getElementById('message-input');
66
+ if (messageInput) {
67
+ messageInput.value = '';
68
+ messageInput.focus();
69
+ }
69
70
 
70
- // Update submit button state
71
- const form = document.querySelector('[data-controller="chats-form"]');
72
- if (form && messageInput) {
73
- const event = new Event('input', { bubbles: true });
74
- messageInput.dispatchEvent(event);
75
- }
71
+ // Update submit button state
72
+ const form = document.querySelector('[data-controller="chats-form"]');
73
+ if (form && messageInput) {
74
+ const event = new Event('input', { bubbles: true });
75
+ messageInput.dispatchEvent(event);
76
+ }
76
77
 
77
- // Scroll to bottom
78
- const chatMessages = document.getElementById('chat-messages');
79
- if (chatMessages) {
80
- chatMessages.scrollTop = chatMessages.scrollHeight;
81
- }
78
+ // Update branch_from_uuid to the latest prompt execution
79
+ const branchField = document.getElementById('branch_from_uuid');
80
+ if (branchField) {
81
+ branchField.value = '<%%= @prompt_execution&.execution_id %>';
82
+ }
83
+
84
+ // Scroll to bottom
85
+ const chatMessages = document.getElementById('chat-messages');
86
+ if (chatMessages) {
87
+ chatMessages.scrollTop = chatMessages.scrollHeight;
88
+ }
89
+ })();
82
90
  </script>
83
91
  </template>
84
92
  </turbo-stream>
@@ -30,7 +30,7 @@
30
30
  id: "message-input",
31
31
  data: { "chats-form-target": "prompt", action: "input->chats-form#updateSubmitButton" } %>
32
32
  </div>
33
- <%%= f.hidden_field :branch_from_uuid, value: params.dig(:chat, :branch_from_uuid) %>
33
+ <%%= f.hidden_field :branch_from_uuid, value: @branch_from_uuid || params.dig(:chat, :branch_from_uuid) %>
34
34
  <div class="button-wrapper">
35
35
  <%%= f.button type: "submit",
36
36
  class: "send-button",
@@ -47,16 +47,18 @@
47
47
 
48
48
 
49
49
  <script>
50
- // Function to handle scrolling for both initial load and Turbo navigation
51
- function scrollChatMessages() {
50
+ // Remove previous listener to prevent duplicates across Turbo navigations
51
+ if (window._scrollChatMessages) {
52
+ document.removeEventListener('turbo:load', window._scrollChatMessages);
53
+ }
54
+
55
+ window._scrollChatMessages = function() {
52
56
  const chatMessages = document.getElementById('chat-messages');
53
57
  if (chatMessages) {
54
58
  <%% if @target_message_id.present? %>
55
- // Scroll to target message
56
59
  const targetMessage = document.getElementById('message-<%%= @target_message_id %>');
57
60
  if (targetMessage) {
58
61
  targetMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
59
- // Highlight the target message
60
62
  targetMessage.style.backgroundColor = '#fef3c7';
61
63
  targetMessage.style.border = '2px solid #fbbf24';
62
64
  setTimeout(() => {
@@ -69,10 +71,8 @@
69
71
  chatMessages.scrollTop = chatMessages.scrollHeight;
70
72
  <%% end %>
71
73
  }
72
- }
74
+ };
73
75
 
74
- // Scroll chat messages to bottom or to target message
75
- // Support both regular page loads and Turbo navigation
76
- document.addEventListener('DOMContentLoaded', scrollChatMessages);
77
- document.addEventListener('turbo:load', scrollChatMessages);
76
+ document.addEventListener('DOMContentLoaded', window._scrollChatMessages);
77
+ document.addEventListener('turbo:load', window._scrollChatMessages);
78
78
  </script>
@@ -30,7 +30,7 @@
30
30
  id: "message-input",
31
31
  data: { "chats-form-target": "prompt", action: "input->chats-form#updateSubmitButton" } %>
32
32
  </div>
33
- <%%= f.hidden_field :branch_from_uuid, value: params.dig(:chat, :branch_from_uuid) %>
33
+ <%%= f.hidden_field :branch_from_uuid, value: @branch_from_uuid || params.dig(:chat, :branch_from_uuid) %>
34
34
  <div class="button-wrapper">
35
35
  <%%= f.button type: "submit",
36
36
  class: "send-button",
@@ -26,13 +26,12 @@
26
26
  <%% # Update history sidebar - replace entire content to ensure update %>
27
27
  <%% if @prompt_execution %>
28
28
  <%%= turbo_stream.replace "history-sidebar" do %>
29
-
30
- <div id="history-content">
29
+ <div id="history-sidebar">
31
30
  <h2>History</h2>
32
31
  <div class="history-stack" id="history-stack" data-controller="history">
33
32
  <%%= render 'prompt_navigator/history_card', locals: {
34
33
  ann: @prompt_execution,
35
- next_ann: nil,
34
+ next_ann: (@chat&.ordered_by_descending_prompt_executions || [])[1],
36
35
  is_active: @prompt_execution.execution_id == @active_message_uuid,
37
36
  card_path: ->(uuid) { prompt_path(uuid) }
38
37
  } %>
@@ -54,25 +53,33 @@
54
53
  <turbo-stream action="after" target="messages-list">
55
54
  <template>
56
55
  <script>
57
- // Clear and refocus message input
58
- const messageInput = document.getElementById('message-input');
59
- if (messageInput) {
60
- messageInput.value = '';
61
- messageInput.focus();
62
- }
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
+ }
63
70
 
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
- }
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
+ }
70
76
 
71
- // Scroll to bottom
72
- const chatMessages = document.getElementById('chat-messages');
73
- if (chatMessages) {
74
- chatMessages.scrollTop = chatMessages.scrollHeight;
75
- }
77
+ // Scroll to bottom
78
+ const chatMessages = document.getElementById('chat-messages');
79
+ if (chatMessages) {
80
+ chatMessages.scrollTop = chatMessages.scrollHeight;
81
+ }
82
+ })();
76
83
  </script>
77
84
  </template>
78
85
  </turbo-stream>
@@ -13,75 +13,16 @@
13
13
  </button>
14
14
  </div>
15
15
  <div class="generation-settings-panel" data-<%%= stimulus_controller %>-target="panel" style="display: none;">
16
- <div class="generation-setting-item">
17
- <label for="temperature">
18
- Temperature
19
- <span class="setting-value" data-<%%= stimulus_controller %>-target="temperatureValue">0.7</span>
20
- </label>
21
- <input type="range" name="temperature" id="temperature"
22
- min="0" max="2" step="0.1" value="0.7"
23
- data-<%%= stimulus_controller %>-target="temperatureRange"
24
- data-action="input-><%%= stimulus_controller %>#updateTemperature">
25
- <div class="setting-range-labels">
26
- <span>0 (deterministic)</span>
27
- <span>2 (creative)</span>
28
- </div>
29
- </div>
30
-
31
- <div class="generation-setting-item">
32
- <label for="top_k">
33
- Top-K
34
- <span class="setting-value" data-<%%= stimulus_controller %>-target="topKValue">40</span>
35
- </label>
36
- <input type="range" name="top_k" id="top_k"
37
- min="1" max="100" step="1" value="40"
38
- data-<%%= stimulus_controller %>-target="topKRange"
39
- data-action="input-><%%= stimulus_controller %>#updateTopK">
40
- <div class="setting-range-labels">
41
- <span>1 (focused)</span>
42
- <span>100 (diverse)</span>
43
- </div>
44
- </div>
45
-
46
- <div class="generation-setting-item">
47
- <label for="top_p">
48
- Top-P
49
- <span class="setting-value" data-<%%= stimulus_controller %>-target="topPValue">0.9</span>
50
- </label>
51
- <input type="range" name="top_p" id="top_p"
52
- min="0" max="1" step="0.05" value="0.9"
53
- data-<%%= stimulus_controller %>-target="topPRange"
54
- data-action="input-><%%= stimulus_controller %>#updateTopP">
55
- <div class="setting-range-labels">
56
- <span>0 (narrow)</span>
57
- <span>1 (broad)</span>
58
- </div>
59
- </div>
60
-
61
- <div class="generation-setting-item">
62
- <label for="max_tokens">
63
- Max Tokens
64
- </label>
65
- <input type="number" name="max_tokens" id="max_tokens"
66
- min="1" max="128000" step="1" value=""
67
- placeholder="Default (model-dependent)"
68
- class="max-tokens-input"
69
- data-<%%= stimulus_controller %>-target="maxTokensInput">
70
- </div>
71
-
72
- <div class="generation-setting-item">
73
- <label for="repeat_penalty">
74
- Repeat Penalty
75
- <span class="setting-value" data-<%%= stimulus_controller %>-target="repeatPenaltyValue">1.1</span>
76
- </label>
77
- <input type="range" name="repeat_penalty" id="repeat_penalty"
78
- min="1" max="2" step="0.05" value="1.1"
79
- data-<%%= stimulus_controller %>-target="repeatPenaltyRange"
80
- data-action="input-><%%= stimulus_controller %>#updateRepeatPenalty">
81
- <div class="setting-range-labels">
82
- <span>1.0 (no penalty)</span>
83
- <span>2.0 (strong)</span>
84
- </div>
16
+ <label for="generation_settings_json" class="generation-settings-label">
17
+ JSON format
18
+ </label>
19
+ <textarea name="generation_settings_json" id="generation_settings_json"
20
+ class="generation-settings-json-input"
21
+ rows="8"
22
+ placeholder='{"temperature": 0.7, "top_k": 40, "top_p": 0.9, "max_tokens": 4096, "repeat_penalty": 1.1}'
23
+ data-<%%= stimulus_controller %>-target="jsonInput"></textarea>
24
+ <div class="generation-settings-hint">
25
+ Available keys: temperature, top_k, top_p, max_tokens, repeat_penalty
85
26
  </div>
86
27
  </div>
87
28
  </div>
@@ -4,8 +4,6 @@ class CreateChats < ActiveRecord::Migration[8.1]
4
4
  t.references :user, null: true, foreign_key: true
5
5
  t.string :uuid, null: false
6
6
  t.string :title
7
- t.string :llm_uuid
8
- t.string :model
9
7
 
10
8
  t.timestamps
11
9
  end
@@ -0,0 +1,7 @@
1
+ class MigrateLlmUuidToPromptExecutions < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :prompt_navigator_prompt_executions, :llm_uuid, :string unless column_exists?(:prompt_navigator_prompt_executions, :llm_uuid)
4
+ remove_column :chats, :llm_uuid, :string if column_exists?(:chats, :llm_uuid)
5
+ remove_column :chats, :model, :string if column_exists?(:chats, :model)
6
+ end
7
+ end
@@ -117,7 +117,7 @@ module LlmMetaClient
117
117
  def ollama_options
118
118
  ollama_list = llms.filter { it["family"] == "ollama" }
119
119
  raise LlmMetaClient::Exceptions::OllamaUnavailableError if ollama_list.empty?
120
- ollama_list
120
+ ollama_list.each { it["llm_type"] ||= "ollama" }
121
121
  end
122
122
 
123
123
  # Builds normalized option hashes from an array of prompts by slicing common keys
@@ -1,3 +1,3 @@
1
1
  module LlmMetaClient
2
- VERSION = "0.6.1"
2
+ VERSION = "1.0.1"
3
3
  end
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: 0.6.1
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dhq_boiler
@@ -49,28 +49,28 @@ dependencies:
49
49
  requirements:
50
50
  - - "~>"
51
51
  - !ruby/object:Gem::Version
52
- version: '0.2'
52
+ version: '1.0'
53
53
  type: :runtime
54
54
  prerelease: false
55
55
  version_requirements: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '0.2'
59
+ version: '1.0'
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: chat_manager
62
62
  requirement: !ruby/object:Gem::Requirement
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: '0.2'
66
+ version: '1.0'
67
67
  type: :runtime
68
68
  prerelease: false
69
69
  version_requirements: !ruby/object:Gem::Requirement
70
70
  requirements:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
- version: '0.2'
73
+ version: '1.0'
74
74
  description: llm_meta_client provides a Rails Engine with scaffold and authentication
75
75
  generators for building LLM-powered chat applications. Supports OpenAI, Anthropic,
76
76
  Google, and Ollama providers.
@@ -130,6 +130,7 @@ files:
130
130
  - lib/generators/llm_meta_client/scaffold/templates/config/initializers/llm_service.rb
131
131
  - lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_chats.rb
132
132
  - lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_messages.rb
133
+ - lib/generators/llm_meta_client/scaffold/templates/db/migrate/migrate_llm_uuid_to_prompt_executions.rb
133
134
  - lib/llm_meta_client.rb
134
135
  - lib/llm_meta_client/chat_manageable.rb
135
136
  - lib/llm_meta_client/engine.rb