iatoolkit 0.3.9__py3-none-any.whl → 0.107.4__py3-none-any.whl

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.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

Files changed (150) hide show
  1. iatoolkit/__init__.py +27 -35
  2. iatoolkit/base_company.py +3 -35
  3. iatoolkit/cli_commands.py +18 -47
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +48 -0
  6. iatoolkit/common/interfaces/__init__.py +0 -0
  7. iatoolkit/common/interfaces/asset_storage.py +34 -0
  8. iatoolkit/common/interfaces/database_provider.py +39 -0
  9. iatoolkit/common/model_registry.py +159 -0
  10. iatoolkit/common/routes.py +138 -0
  11. iatoolkit/common/session_manager.py +26 -0
  12. iatoolkit/common/util.py +353 -0
  13. iatoolkit/company_registry.py +66 -29
  14. iatoolkit/core.py +514 -0
  15. iatoolkit/infra/__init__.py +5 -0
  16. iatoolkit/infra/brevo_mail_app.py +123 -0
  17. iatoolkit/infra/call_service.py +140 -0
  18. iatoolkit/infra/connectors/__init__.py +5 -0
  19. iatoolkit/infra/connectors/file_connector.py +17 -0
  20. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  21. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  22. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  23. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  24. iatoolkit/infra/connectors/s3_connector.py +33 -0
  25. iatoolkit/infra/google_chat_app.py +57 -0
  26. iatoolkit/infra/llm_providers/__init__.py +0 -0
  27. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  28. iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
  29. iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
  30. iatoolkit/infra/llm_proxy.py +268 -0
  31. iatoolkit/infra/llm_response.py +45 -0
  32. iatoolkit/infra/redis_session_manager.py +122 -0
  33. iatoolkit/locales/en.yaml +222 -0
  34. iatoolkit/locales/es.yaml +225 -0
  35. iatoolkit/repositories/__init__.py +5 -0
  36. iatoolkit/repositories/database_manager.py +187 -0
  37. iatoolkit/repositories/document_repo.py +33 -0
  38. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  39. iatoolkit/repositories/llm_query_repo.py +105 -0
  40. iatoolkit/repositories/models.py +279 -0
  41. iatoolkit/repositories/profile_repo.py +171 -0
  42. iatoolkit/repositories/vs_repo.py +150 -0
  43. iatoolkit/services/__init__.py +5 -0
  44. iatoolkit/services/auth_service.py +193 -0
  45. {services → iatoolkit/services}/benchmark_service.py +7 -7
  46. iatoolkit/services/branding_service.py +153 -0
  47. iatoolkit/services/company_context_service.py +214 -0
  48. iatoolkit/services/configuration_service.py +375 -0
  49. iatoolkit/services/dispatcher_service.py +134 -0
  50. {services → iatoolkit/services}/document_service.py +20 -8
  51. iatoolkit/services/embedding_service.py +148 -0
  52. iatoolkit/services/excel_service.py +156 -0
  53. {services → iatoolkit/services}/file_processor_service.py +36 -21
  54. iatoolkit/services/history_manager_service.py +208 -0
  55. iatoolkit/services/i18n_service.py +104 -0
  56. iatoolkit/services/jwt_service.py +80 -0
  57. iatoolkit/services/language_service.py +89 -0
  58. iatoolkit/services/license_service.py +82 -0
  59. iatoolkit/services/llm_client_service.py +438 -0
  60. iatoolkit/services/load_documents_service.py +174 -0
  61. iatoolkit/services/mail_service.py +213 -0
  62. {services → iatoolkit/services}/profile_service.py +200 -101
  63. iatoolkit/services/prompt_service.py +303 -0
  64. iatoolkit/services/query_service.py +467 -0
  65. iatoolkit/services/search_service.py +55 -0
  66. iatoolkit/services/sql_service.py +169 -0
  67. iatoolkit/services/tool_service.py +246 -0
  68. iatoolkit/services/user_feedback_service.py +117 -0
  69. iatoolkit/services/user_session_context_service.py +213 -0
  70. iatoolkit/static/images/fernando.jpeg +0 -0
  71. iatoolkit/static/images/iatoolkit_core.png +0 -0
  72. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  73. iatoolkit/static/js/chat_feedback_button.js +80 -0
  74. iatoolkit/static/js/chat_filepond.js +85 -0
  75. iatoolkit/static/js/chat_help_content.js +124 -0
  76. iatoolkit/static/js/chat_history_button.js +110 -0
  77. iatoolkit/static/js/chat_logout_button.js +36 -0
  78. iatoolkit/static/js/chat_main.js +401 -0
  79. iatoolkit/static/js/chat_model_selector.js +227 -0
  80. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  81. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  82. iatoolkit/static/js/chat_reload_button.js +38 -0
  83. iatoolkit/static/styles/chat_iatoolkit.css +559 -0
  84. iatoolkit/static/styles/chat_modal.css +133 -0
  85. iatoolkit/static/styles/chat_public.css +135 -0
  86. iatoolkit/static/styles/documents.css +598 -0
  87. iatoolkit/static/styles/landing_page.css +398 -0
  88. iatoolkit/static/styles/llm_output.css +148 -0
  89. iatoolkit/static/styles/onboarding.css +176 -0
  90. iatoolkit/system_prompts/__init__.py +0 -0
  91. iatoolkit/system_prompts/query_main.prompt +30 -23
  92. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  93. iatoolkit/templates/_company_header.html +45 -0
  94. iatoolkit/templates/_login_widget.html +42 -0
  95. iatoolkit/templates/base.html +78 -0
  96. iatoolkit/templates/change_password.html +66 -0
  97. iatoolkit/templates/chat.html +337 -0
  98. iatoolkit/templates/chat_modals.html +185 -0
  99. iatoolkit/templates/error.html +51 -0
  100. iatoolkit/templates/forgot_password.html +51 -0
  101. iatoolkit/templates/onboarding_shell.html +106 -0
  102. iatoolkit/templates/signup.html +79 -0
  103. iatoolkit/views/__init__.py +5 -0
  104. iatoolkit/views/base_login_view.py +96 -0
  105. iatoolkit/views/change_password_view.py +116 -0
  106. iatoolkit/views/chat_view.py +76 -0
  107. iatoolkit/views/embedding_api_view.py +65 -0
  108. iatoolkit/views/forgot_password_view.py +75 -0
  109. iatoolkit/views/help_content_api_view.py +54 -0
  110. iatoolkit/views/history_api_view.py +56 -0
  111. iatoolkit/views/home_view.py +63 -0
  112. iatoolkit/views/init_context_api_view.py +74 -0
  113. iatoolkit/views/llmquery_api_view.py +59 -0
  114. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  115. iatoolkit/views/load_document_api_view.py +65 -0
  116. iatoolkit/views/login_view.py +170 -0
  117. iatoolkit/views/logout_api_view.py +57 -0
  118. iatoolkit/views/profile_api_view.py +46 -0
  119. iatoolkit/views/prompt_api_view.py +37 -0
  120. iatoolkit/views/root_redirect_view.py +22 -0
  121. iatoolkit/views/signup_view.py +100 -0
  122. iatoolkit/views/static_page_view.py +27 -0
  123. iatoolkit/views/user_feedback_api_view.py +60 -0
  124. iatoolkit/views/users_api_view.py +33 -0
  125. iatoolkit/views/verify_user_view.py +60 -0
  126. iatoolkit-0.107.4.dist-info/METADATA +268 -0
  127. iatoolkit-0.107.4.dist-info/RECORD +132 -0
  128. iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
  129. iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  130. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
  131. iatoolkit/iatoolkit.py +0 -413
  132. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  133. iatoolkit-0.3.9.dist-info/METADATA +0 -252
  134. iatoolkit-0.3.9.dist-info/RECORD +0 -32
  135. services/__init__.py +0 -5
  136. services/api_service.py +0 -75
  137. services/dispatcher_service.py +0 -351
  138. services/excel_service.py +0 -98
  139. services/history_service.py +0 -45
  140. services/jwt_service.py +0 -91
  141. services/load_documents_service.py +0 -212
  142. services/mail_service.py +0 -62
  143. services/prompt_manager_service.py +0 -172
  144. services/query_service.py +0 -334
  145. services/search_service.py +0 -32
  146. services/sql_service.py +0 -42
  147. services/tasks_service.py +0 -188
  148. services/user_feedback_service.py +0 -67
  149. services/user_session_context_service.py +0 -85
  150. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,401 @@
1
+ // Global variables for request management
2
+ let isRequestInProgress = false;
3
+ let abortController = null;
4
+ let selectedPrompt = null; // Will hold a lightweight prompt object
5
+
6
+ $(document).ready(function () {
7
+ // Si viene un Token retornado por login con APY-KEY se gatilla el redeem a una sesion de flask
8
+ if (window.redeemToken) {
9
+ const url = '/api/redeem_token';
10
+ // No await: dejamos que callToolkit maneje todo internamente
11
+ callToolkit(url, {'token': window.redeemToken}, "POST").catch(() => {});
12
+ }
13
+
14
+ const layoutContainer = document.querySelector('.chat-layout-container');
15
+ const promptAssistantCollapse = document.getElementById('prompt-assistant-collapse');
16
+
17
+ if (layoutContainer && promptAssistantCollapse) {
18
+ promptAssistantCollapse.addEventListener('show.bs.collapse', function () {
19
+ layoutContainer.classList.add('prompt-assistant-open');
20
+ setTimeout(() => {
21
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
22
+ }, 300);
23
+ });
24
+
25
+ promptAssistantCollapse.addEventListener('hide.bs.collapse', function () {
26
+ layoutContainer.classList.remove('prompt-assistant-open');
27
+ });
28
+ }
29
+
30
+ // --- chat main event hadlers ---
31
+ $('#send-button').on('click', handleChatMessage);
32
+ $('#stop-button').on('click', abortCurrentRequest);
33
+ if (window.sendButtonColor)
34
+ $('#send-button i').css('color', window.sendButtonColor);
35
+
36
+
37
+ // Handles Enter key press to send a message
38
+ const questionTextarea = $('#question');
39
+ questionTextarea.on('keypress', function (event) {
40
+ if (event.key === 'Enter' && !event.shiftKey) {
41
+ event.preventDefault();
42
+ handleChatMessage();
43
+ }
44
+ });
45
+
46
+ // Handles auto-resizing and enables the send button on input
47
+ questionTextarea.on('input', function () {
48
+ autoResizeTextarea(this);
49
+ // If the user types, it overrides any prompt selection
50
+ if (selectedPrompt) {
51
+ resetPromptSelection();
52
+ }
53
+ updateSendButtonState();
54
+ });
55
+
56
+ // Set the initial disabled state of the send button
57
+ updateSendButtonState();
58
+
59
+ });
60
+
61
+
62
+ /**
63
+ * Main function to handle sending a chat message.
64
+ */
65
+ const handleChatMessage = async function () {
66
+ if (isRequestInProgress || $('#send-button').hasClass('disabled')) {
67
+ return;
68
+ }
69
+
70
+ isRequestInProgress = true;
71
+ toggleSendStopButtons(true);
72
+
73
+ try {
74
+ const question = $('#question').val().trim();
75
+ const promptName = selectedPrompt ? selectedPrompt.prompt : null;
76
+
77
+ let displayMessage = question;
78
+ let isEditable = true;
79
+ const clientData = {};
80
+
81
+ if (selectedPrompt) {
82
+ displayMessage = selectedPrompt.description;
83
+ isEditable = false;
84
+
85
+ (selectedPrompt.custom_fields || []).forEach(field => {
86
+ const value = $('#' + field.data_key + '-id').val().trim();
87
+ if (value) {
88
+ clientData[field.data_key] = value;
89
+ }
90
+ });
91
+
92
+ const paramsString = Object.values(clientData).join(', ');
93
+ if (paramsString) { displayMessage += `: ${paramsString}`; }
94
+ }
95
+
96
+ // Simplificado: Si no hay mensaje, el 'finally' se encargará de limpiar.
97
+ if (!displayMessage) {
98
+ return;
99
+ }
100
+
101
+ displayUserMessage(displayMessage, isEditable, question);
102
+ showSpinner();
103
+ resetAllInputs();
104
+
105
+ const files = window.filePond.getFiles();
106
+ const filesBase64 = await Promise.all(files.map(fileItem => toBase64(fileItem.file)));
107
+
108
+ const data = {
109
+ question: question,
110
+ prompt_name: promptName,
111
+ client_data: clientData,
112
+ files: filesBase64.map(f => ({ filename: f.name, content: f.base64 })),
113
+ user_identifier: window.user_identifier,
114
+ model: (window.currentLlmModel || window.defaultLlmModel || '')
115
+
116
+ };
117
+
118
+ const responseData = await callToolkit("/api/llm_query", data, "POST");
119
+ if (responseData && responseData.answer) {
120
+ // CAMBIO: contenedor principal para la respuesta del bot
121
+ const botMessageContainer = $('<div>').addClass('bot-message-container');
122
+
123
+ // 1. Si hay reasoning_content, agregar el acordeón colapsable
124
+ if (responseData.reasoning_content) {
125
+ const uniqueId = 'reasoning-' + Date.now(); // ID único para el collapse
126
+
127
+ const reasoningBlock = $(`
128
+ <div class="reasoning-block">
129
+ <button class="reasoning-toggle btn btn-sm btn-link text-decoration-none p-0"
130
+ type="button"
131
+ data-bs-toggle="collapse"
132
+ data-bs-target="#${uniqueId}"
133
+ aria-expanded="false"
134
+ aria-controls="${uniqueId}">
135
+ <i class="bi bi-lightbulb me-1"></i> ${t_js('show_reasoning')}
136
+ </button>
137
+
138
+ <div class="collapse mt-2" id="${uniqueId}">
139
+ <div class="reasoning-card">
140
+ ${responseData.reasoning_content}
141
+ </div>
142
+ </div>
143
+ </div>
144
+ `);
145
+ botMessageContainer.append(reasoningBlock);
146
+ }
147
+
148
+ // 2. Agregar la respuesta final
149
+ const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
150
+ botMessageContainer.append(answerSection);
151
+
152
+ // 3. Mostrar el contenedor completo
153
+ displayBotMessage(botMessageContainer);
154
+
155
+ }
156
+ } catch (error) {
157
+ if (error.name === 'AbortError') {
158
+
159
+ // Usando jQuery estándar para construir el elemento ---
160
+ const icon = $('<i>').addClass('bi bi-stop-circle me-2'); // Icono sin "fill" para un look más ligero
161
+ const textSpan = $('<span>').text('La generación de la respuesta ha sido detenida.');
162
+
163
+ const abortMessage = $('<div>')
164
+ .addClass('system-message')
165
+ .append(icon)
166
+ .append(textSpan);
167
+
168
+ displayBotMessage(abortMessage);
169
+ } else {
170
+ console.error("Error in handleChatMessage:", error);
171
+ const errorSection = $('<div>').addClass('error-section').append('<p>Ocurrió un error al procesar la solicitud.</p>');
172
+ displayBotMessage(errorSection);
173
+ }
174
+ } finally {
175
+ // Este bloque se ejecuta siempre, garantizando que el estado se limpie.
176
+ isRequestInProgress = false;
177
+ hideSpinner();
178
+ toggleSendStopButtons(false);
179
+ updateSendButtonState();
180
+ if (window.filePond) {
181
+ window.filePond.removeFiles();
182
+ }
183
+ }
184
+ };
185
+
186
+
187
+ /**
188
+ * Resets all inputs to their initial state.
189
+ */
190
+ function resetAllInputs() {
191
+ resetPromptSelection();
192
+ $('#question').val('');
193
+ autoResizeTextarea($('#question')[0]);
194
+
195
+ const promptCollapseEl = document.getElementById('prompt-assistant-collapse');
196
+ const promptCollapse = bootstrap.Collapse.getInstance(promptCollapseEl);
197
+ if (promptCollapse) {
198
+ promptCollapse.hide();
199
+ }
200
+
201
+ updateSendButtonState();
202
+ }
203
+
204
+ /**
205
+ * Enables or disables the send button based on whether there's content
206
+ * in the textarea or a prompt has been selected.
207
+ */
208
+ function updateSendButtonState() {
209
+ const question = $('#question').val().trim();
210
+ const isPromptSelected = selectedPrompt !== null;
211
+
212
+ if (isPromptSelected || question) {
213
+ $('#send-button').removeClass('disabled');
214
+ } else {
215
+ $('#send-button').addClass('disabled');
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Auto-resizes the textarea to fit its content.
221
+ */
222
+ function autoResizeTextarea(element) {
223
+ element.style.height = 'auto';
224
+ element.style.height = (element.scrollHeight) + 'px';
225
+ }
226
+
227
+ /**
228
+ * Toggles the main action button between 'Send' and 'Stop'.
229
+ * @param {boolean} showStop - If true, shows the Stop button. Otherwise, shows the Send button.
230
+ */
231
+ const toggleSendStopButtons = function (showStop) {
232
+ $('#send-button-container').toggle(!showStop);
233
+ $('#stop-button-container').toggle(showStop);
234
+ };
235
+
236
+ /**
237
+ * Generic function to make API calls to the backend.
238
+ * @param {string} apiPath - The API endpoint path.
239
+ * @param {object} data - The data payload to send.
240
+ * @param {string} method - The HTTP method (e.g., 'POST').
241
+ * @param {number} timeoutMs - Timeout in milliseconds.
242
+ * @returns {Promise<object|null>} The response data or null on error.
243
+ */
244
+ const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
245
+ // normalize the url for avoiding double //
246
+ const base = (window.iatoolkit_base_url || '').replace(/\/+$/, '');
247
+ const company = (window.companyShortName || '').replace(/^\/+|\/+$/g, '');
248
+ const path = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
249
+ const url = `${base}/${company}${path}`;
250
+
251
+
252
+ abortController = new AbortController();
253
+ const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
254
+
255
+ try {
256
+ const fetchOptions = {
257
+ method: method,
258
+ signal: abortController.signal,
259
+ credentials: 'include'
260
+ };
261
+
262
+ // Solo agrega body si el método lo soporta y hay datos
263
+ const methodUpper = (method || '').toUpperCase();
264
+ const canHaveBody = !['GET', 'HEAD'].includes(methodUpper);
265
+ if (canHaveBody && data !== undefined && data !== null) {
266
+ fetchOptions.body = JSON.stringify(data);
267
+ fetchOptions.headers = {"Content-Type": "application/json"};
268
+
269
+ }
270
+ const response = await fetch(url, fetchOptions);
271
+ clearTimeout(timeoutId);
272
+
273
+ // answer is NOT OK (status != 200)
274
+ if (!response.ok) {
275
+ try {
276
+ // Intentamos leer el error como JSON, que es el formato esperado de nuestra API.
277
+ const errorData = await response.json();
278
+
279
+ // if it's a iatoolkit error (409 o 400 with a message), shot it on the chat
280
+ if (errorData && (errorData.error_message || errorData.error)) {
281
+ const errorMessage = errorData.error_message || errorData.error || t_js('unknown_server_error');
282
+ const errorIcon = '<i class="bi bi-exclamation-triangle"></i>';
283
+ const endpointError = $('<div>').addClass('error-section').html(errorIcon + `<p>${errorMessage}</p>`);
284
+ displayBotMessage(endpointError);
285
+ } else {
286
+ // if there is not message, we show a generic error message
287
+ throw new Error(`Server error: ${response.status}`);
288
+ }
289
+ } catch (e) {
290
+ // Si response.json() falla, es porque el cuerpo no era JSON (ej. un 502 con HTML).
291
+ // Mostramos un error genérico y más claro para el usuario.
292
+ const errorMessage = `Error de comunicación con el servidor (${response.status}). Por favor, intente de nuevo más tarde.`;
293
+ toastr.error(errorMessage);
294
+ }
295
+
296
+ // stop the flow on the calling function
297
+ return null;
298
+ }
299
+
300
+ // if the answer is OK
301
+ return await response.json();
302
+ } catch (error) {
303
+ clearTimeout(timeoutId);
304
+ if (error.name === 'AbortError') {
305
+ throw error; // Re-throw to be handled by handleChatMessage
306
+ } else {
307
+ toastr.error(t_js('network_error') );
308
+ }
309
+ return null;
310
+ }
311
+ };
312
+
313
+
314
+ /**
315
+ * Displays the user's message in the chat container.
316
+ * @param {string} message - The full message string to display.
317
+ * @param {boolean} isEditable - Determines if the edit icon should be shown.
318
+ * @param {string} originalQuestion - The original text to put back in the textarea for editing.
319
+ */
320
+ const displayUserMessage = function(message, isEditable, originalQuestion) {
321
+ const chatContainer = $('#chat-container');
322
+ const userMessage = $('<div>').addClass('message shadow-sm');
323
+ const messageText = $('<span>').text(message);
324
+
325
+ userMessage.append(messageText);
326
+
327
+ if (isEditable) {
328
+ const editIcon = $('<i>').addClass('p-2 bi bi-pencil-fill edit-icon edit-pencil').attr('title', 'Edit query').on('click', function () {
329
+ $('#question').val(originalQuestion)
330
+ autoResizeTextarea($('#question')[0]);
331
+ $('#send-button').removeClass('disabled');
332
+
333
+ if (window.innerWidth > 768)
334
+ $('#question').focus();
335
+ });
336
+ userMessage.append(editIcon);
337
+ }
338
+ chatContainer.append(userMessage);
339
+ chatContainer.scrollTop(chatContainer[0].scrollHeight);
340
+ };
341
+
342
+ /**
343
+ * Appends a message from the bot to the chat container.
344
+ * @param {jQuery} section - The jQuery object to append.
345
+ */
346
+ function displayBotMessage(section) {
347
+ const chatContainer = $('#chat-container');
348
+ chatContainer.append(section);
349
+ chatContainer.scrollTop(chatContainer[0].scrollHeight);
350
+ }
351
+
352
+ /**
353
+ * Aborts the current in-progress API request.
354
+ */
355
+ const abortCurrentRequest = function () {
356
+ if (isRequestInProgress && abortController) {
357
+ abortController.abort();
358
+ }
359
+ };
360
+
361
+ /**
362
+ * Shows the loading spinner in the chat.
363
+ */
364
+ const showSpinner = function () {
365
+ if ($('#spinner').length) return;
366
+ const accessibilityClass = (typeof bootstrap !== 'undefined') ? 'visually-hidden' : 'sr-only';
367
+ const spinnerText = t_js('loading');
368
+ const spinner = $(`
369
+ <div id="spinner" style="display: flex; align-items: center; justify-content: start; margin: 10px 0; padding: 10px;">
370
+ <div class="spinner-border" role="status" style="width: 1.5rem; height: 1.5rem; margin-right: 15px;">
371
+ <span class="${accessibilityClass}">Loading...</span>
372
+ </div>
373
+ <span style="font-weight: bold; font-size: 15px;">${spinnerText}</span>
374
+ </div>
375
+ `);
376
+ $('#chat-container').append(spinner).scrollTop($('#chat-container')[0].scrollHeight);
377
+ };
378
+
379
+ /**
380
+ * Hides the loading spinner.
381
+ */
382
+ function hideSpinner() {
383
+ $('#spinner').fadeOut(function () {
384
+ $(this).remove();
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Converts a File object to a Base64 encoded string.
390
+ * @param {File} file The file to convert.
391
+ * @returns {Promise<{name: string, base64: string}>}
392
+ */
393
+ function toBase64(file) {
394
+ return new Promise((resolve, reject) => {
395
+ const reader = new FileReader();
396
+ reader.onload = () => resolve({name: file.name, base64: reader.result.split(",")[1]});
397
+ reader.onerror = reject;
398
+ reader.readAsDataURL(file);
399
+ });
400
+ }
401
+
@@ -0,0 +1,227 @@
1
+ // src/iatoolkit/static/js/chat_model_selector.js
2
+ // Gestión del selector de modelo LLM en la barra superior.
3
+
4
+ // Estado global del modelo actual (visible también para otros scripts)
5
+ window.currentLlmModel = window.currentLlmModel || null;
6
+
7
+ (function () {
8
+ /**
9
+ * Lee el modelo guardado en localStorage (si existe y es válido).
10
+ */
11
+ function loadStoredModelId() {
12
+ try {
13
+ const stored = localStorage.getItem('iatoolkit.selected_llm_model');
14
+ return stored || null;
15
+ } catch (e) {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Guarda el modelo seleccionado en localStorage para esta instancia de navegador.
22
+ * No es crítico: si falla, simplemente no persistimos.
23
+ */
24
+ function storeModelId(modelId) {
25
+ try {
26
+ if (!modelId) {
27
+ localStorage.removeItem('iatoolkit.selected_llm_model');
28
+ } else {
29
+ localStorage.setItem('iatoolkit.selected_llm_model', modelId);
30
+ }
31
+ } catch (e) {
32
+ // No hacemos nada: fallo silencioso
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Devuelve la lista de modelos disponibles desde la variable global.
38
+ */
39
+ function getAvailableModels() {
40
+ const raw = window.availableLlmModels;
41
+ if (!Array.isArray(raw)) {
42
+ return [];
43
+ }
44
+ return raw.map(m => ({
45
+ id: m.id,
46
+ label: m.label || m.id,
47
+ description: m.description || ''
48
+ })).filter(m => !!m.id);
49
+ }
50
+
51
+ /**
52
+ * Inicializa el estado de currentLlmModel usando SIEMPRE la config de company.yaml:
53
+ * 1) defaultLlmModel (company.yaml)
54
+ * 2) si no existe o no está en la lista, usa el primer modelo disponible.
55
+ *
56
+ * No se lee nada de localStorage en este punto: cada apertura de chat
57
+ * arranca desde la configuración de la compañía.
58
+ */
59
+ function initCurrentModel() {
60
+ const models = getAvailableModels();
61
+ const defaultId = (window.defaultLlmModel || '').trim() || null;
62
+
63
+ let resolved = null;
64
+
65
+ if (defaultId && models.some(m => m.id === defaultId)) {
66
+ resolved = defaultId;
67
+ } else if (models.length > 0) {
68
+ resolved = models[0].id;
69
+ }
70
+
71
+ window.currentLlmModel = resolved;
72
+ return resolved;
73
+ }
74
+
75
+ /**
76
+ * Pinta la lista de modelos en el popup y marca el seleccionado.
77
+ */
78
+ function renderModelList() {
79
+ const listEl = document.getElementById('llm-model-list');
80
+ if (!listEl) return;
81
+
82
+ const models = getAvailableModels();
83
+ const activeId = window.currentLlmModel;
84
+ listEl.innerHTML = '';
85
+
86
+ if (!models.length) {
87
+ const emptyItem = document.createElement('div');
88
+ emptyItem.className = 'list-group-item small text-muted';
89
+ emptyItem.textContent = 'No hay modelos configurados.';
90
+ listEl.appendChild(emptyItem);
91
+ return;
92
+ }
93
+
94
+ models.forEach(model => {
95
+ const item = document.createElement('button');
96
+ item.type = 'button';
97
+ item.className = 'list-group-item list-group-item-action small';
98
+
99
+ const isActive = model.id === activeId;
100
+ if (isActive) {
101
+ item.classList.add('active');
102
+ }
103
+
104
+ item.innerHTML = `
105
+ <div class="d-flex justify-content-between align-items-center">
106
+ <div>
107
+ <div class="fw-semibold">${model.label}</div>
108
+ ${model.description
109
+ ? `<div class="text-muted" style="font-size: 0.8rem;">${model.description}</div>`
110
+ : ''
111
+ }
112
+ </div>
113
+ ${isActive ? '<i class="bi bi-check-lg ms-2"></i>' : ''}
114
+ </div>
115
+ `;
116
+
117
+ item.addEventListener('click', () => {
118
+ selectModel(model.id);
119
+ });
120
+
121
+ listEl.appendChild(item);
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Actualiza el label del botón principal con el modelo actual.
127
+ */
128
+ function updateButtonLabel() {
129
+ const labelEl = document.getElementById('llm-model-button-label');
130
+ if (!labelEl) return;
131
+
132
+ const models = getAvailableModels();
133
+ const activeId = window.currentLlmModel;
134
+ const activeModel = models.find(m => m.id === activeId);
135
+
136
+ if (activeModel) {
137
+ labelEl.textContent = activeModel.label;
138
+ } else if (window.defaultLlmModel) {
139
+ labelEl.textContent = window.defaultLlmModel;
140
+ } else {
141
+ labelEl.textContent = 'Modelo IA';
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Selecciona un modelo: actualiza estado global, UI y almacenamiento local.
147
+ */
148
+ function selectModel(modelId) {
149
+ if (!modelId) return;
150
+
151
+ const models = getAvailableModels();
152
+ const exists = models.some(m => m.id === modelId);
153
+ if (!exists) return;
154
+
155
+ window.currentLlmModel = modelId;
156
+ storeModelId(modelId);
157
+ updateButtonLabel();
158
+ renderModelList();
159
+ hidePopup();
160
+
161
+ if (typeof toastr !== 'undefined') {
162
+ toastr.info(`Modelo actualizado a "${models.find(m => m.id === modelId).label}".`);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Muestra/oculta el popup anclado al botón.
168
+ */
169
+ function togglePopup() {
170
+ const popup = document.getElementById('llm-model-popup');
171
+ const btn = document.getElementById('llm-model-button');
172
+ if (!popup || !btn) return;
173
+
174
+ const isVisible = popup.style.display === 'block';
175
+
176
+ if (isVisible) {
177
+ hidePopup();
178
+ } else {
179
+ const rect = btn.getBoundingClientRect();
180
+ popup.style.display = 'block';
181
+
182
+ // Posicionamos justo debajo del botón, alineado a la izquierda
183
+ popup.style.top = `${rect.bottom + window.scrollY + 4}px`;
184
+ popup.style.left = `${rect.left + window.scrollX}px`;
185
+ }
186
+ }
187
+
188
+ function hidePopup() {
189
+ const popup = document.getElementById('llm-model-popup');
190
+ if (popup) {
191
+ popup.style.display = 'none';
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Cierra el popup si el usuario hace click fuera.
197
+ */
198
+ function setupOutsideClickHandler() {
199
+ document.addEventListener('click', (event) => {
200
+ const popup = document.getElementById('llm-model-popup');
201
+ const btn = document.getElementById('llm-model-button');
202
+ if (!popup || !btn) return;
203
+
204
+ if (popup.style.display !== 'block') return;
205
+
206
+ if (!popup.contains(event.target) && !btn.contains(event.target)) {
207
+ hidePopup();
208
+ }
209
+ });
210
+ }
211
+
212
+ document.addEventListener('DOMContentLoaded', () => {
213
+ // Inicializar estado inicial del modelo
214
+ initCurrentModel();
215
+ updateButtonLabel();
216
+ renderModelList();
217
+ setupOutsideClickHandler();
218
+
219
+ const btn = document.getElementById('llm-model-button');
220
+ if (btn) {
221
+ btn.addEventListener('click', (event) => {
222
+ event.stopPropagation();
223
+ togglePopup();
224
+ });
225
+ }
226
+ });
227
+ })();