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.
- iatoolkit/__init__.py +27 -35
- iatoolkit/base_company.py +3 -35
- iatoolkit/cli_commands.py +18 -47
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +48 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +39 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +138 -0
- iatoolkit/common/session_manager.py +26 -0
- iatoolkit/common/util.py +353 -0
- iatoolkit/company_registry.py +66 -29
- iatoolkit/core.py +514 -0
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/brevo_mail_app.py +123 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
- iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
- iatoolkit/infra/llm_proxy.py +268 -0
- iatoolkit/infra/llm_response.py +45 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +222 -0
- iatoolkit/locales/es.yaml +225 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +187 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +105 -0
- iatoolkit/repositories/models.py +279 -0
- iatoolkit/repositories/profile_repo.py +171 -0
- iatoolkit/repositories/vs_repo.py +150 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +7 -7
- iatoolkit/services/branding_service.py +153 -0
- iatoolkit/services/company_context_service.py +214 -0
- iatoolkit/services/configuration_service.py +375 -0
- iatoolkit/services/dispatcher_service.py +134 -0
- {services → iatoolkit/services}/document_service.py +20 -8
- iatoolkit/services/embedding_service.py +148 -0
- iatoolkit/services/excel_service.py +156 -0
- {services → iatoolkit/services}/file_processor_service.py +36 -21
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +80 -0
- iatoolkit/services/language_service.py +89 -0
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/llm_client_service.py +438 -0
- iatoolkit/services/load_documents_service.py +174 -0
- iatoolkit/services/mail_service.py +213 -0
- {services → iatoolkit/services}/profile_service.py +200 -101
- iatoolkit/services/prompt_service.py +303 -0
- iatoolkit/services/query_service.py +467 -0
- iatoolkit/services/search_service.py +55 -0
- iatoolkit/services/sql_service.py +169 -0
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +117 -0
- iatoolkit/services/user_session_context_service.py +213 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +401 -0
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +103 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +38 -0
- iatoolkit/static/styles/chat_iatoolkit.css +559 -0
- iatoolkit/static/styles/chat_modal.css +133 -0
- iatoolkit/static/styles/chat_public.css +135 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +398 -0
- iatoolkit/static/styles/llm_output.css +148 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +30 -23
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +45 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +78 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +337 -0
- iatoolkit/templates/chat_modals.html +185 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +51 -0
- iatoolkit/templates/onboarding_shell.html +106 -0
- iatoolkit/templates/signup.html +79 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +96 -0
- iatoolkit/views/change_password_view.py +116 -0
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +75 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +63 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +59 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +65 -0
- iatoolkit/views/login_view.py +170 -0
- iatoolkit/views/logout_api_view.py +57 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +100 -0
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +60 -0
- iatoolkit-0.107.4.dist-info/METADATA +268 -0
- iatoolkit-0.107.4.dist-info/RECORD +132 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
- iatoolkit/iatoolkit.py +0 -413
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.3.9.dist-info/METADATA +0 -252
- iatoolkit-0.3.9.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/dispatcher_service.py +0 -351
- services/excel_service.py +0 -98
- services/history_service.py +0 -45
- services/jwt_service.py +0 -91
- services/load_documents_service.py +0 -212
- services/mail_service.py +0 -62
- services/prompt_manager_service.py +0 -172
- services/query_service.py +0 -334
- services/search_service.py +0 -32
- services/sql_service.py +0 -42
- services/tasks_service.py +0 -188
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {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
|
+
})();
|