iatoolkit 0.11.0__py3-none-any.whl → 0.71.2__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.
Files changed (122) hide show
  1. iatoolkit/__init__.py +2 -6
  2. iatoolkit/base_company.py +9 -29
  3. iatoolkit/cli_commands.py +1 -1
  4. iatoolkit/common/routes.py +96 -52
  5. iatoolkit/common/session_manager.py +2 -1
  6. iatoolkit/common/util.py +17 -27
  7. iatoolkit/company_registry.py +1 -2
  8. iatoolkit/iatoolkit.py +97 -53
  9. iatoolkit/infra/llm_client.py +15 -20
  10. iatoolkit/infra/llm_proxy.py +38 -10
  11. iatoolkit/infra/openai_adapter.py +1 -1
  12. iatoolkit/infra/redis_session_manager.py +48 -2
  13. iatoolkit/locales/en.yaml +167 -0
  14. iatoolkit/locales/es.yaml +163 -0
  15. iatoolkit/repositories/database_manager.py +23 -3
  16. iatoolkit/repositories/document_repo.py +1 -1
  17. iatoolkit/repositories/models.py +35 -10
  18. iatoolkit/repositories/profile_repo.py +3 -2
  19. iatoolkit/repositories/vs_repo.py +26 -20
  20. iatoolkit/services/auth_service.py +193 -0
  21. iatoolkit/services/branding_service.py +70 -25
  22. iatoolkit/services/company_context_service.py +155 -0
  23. iatoolkit/services/configuration_service.py +133 -0
  24. iatoolkit/services/dispatcher_service.py +80 -105
  25. iatoolkit/services/document_service.py +5 -2
  26. iatoolkit/services/embedding_service.py +146 -0
  27. iatoolkit/services/excel_service.py +30 -26
  28. iatoolkit/services/file_processor_service.py +4 -12
  29. iatoolkit/services/history_service.py +7 -16
  30. iatoolkit/services/i18n_service.py +104 -0
  31. iatoolkit/services/jwt_service.py +18 -29
  32. iatoolkit/services/language_service.py +83 -0
  33. iatoolkit/services/load_documents_service.py +100 -113
  34. iatoolkit/services/mail_service.py +9 -4
  35. iatoolkit/services/profile_service.py +152 -76
  36. iatoolkit/services/prompt_manager_service.py +20 -16
  37. iatoolkit/services/query_service.py +208 -96
  38. iatoolkit/services/search_service.py +11 -4
  39. iatoolkit/services/sql_service.py +57 -25
  40. iatoolkit/services/tasks_service.py +1 -1
  41. iatoolkit/services/user_feedback_service.py +72 -34
  42. iatoolkit/services/user_session_context_service.py +112 -54
  43. iatoolkit/static/images/fernando.jpeg +0 -0
  44. iatoolkit/static/js/chat_feedback_button.js +80 -0
  45. iatoolkit/static/js/chat_help_content.js +124 -0
  46. iatoolkit/static/js/chat_history_button.js +110 -0
  47. iatoolkit/static/js/chat_logout_button.js +36 -0
  48. iatoolkit/static/js/chat_main.js +135 -222
  49. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  50. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  51. iatoolkit/static/js/chat_reload_button.js +35 -0
  52. iatoolkit/static/styles/chat_iatoolkit.css +289 -210
  53. iatoolkit/static/styles/chat_modal.css +63 -77
  54. iatoolkit/static/styles/chat_public.css +107 -0
  55. iatoolkit/static/styles/landing_page.css +182 -0
  56. iatoolkit/static/styles/onboarding.css +176 -0
  57. iatoolkit/system_prompts/query_main.prompt +5 -22
  58. iatoolkit/templates/_company_header.html +20 -0
  59. iatoolkit/templates/_login_widget.html +42 -0
  60. iatoolkit/templates/base.html +40 -20
  61. iatoolkit/templates/change_password.html +57 -36
  62. iatoolkit/templates/chat.html +180 -86
  63. iatoolkit/templates/chat_modals.html +138 -68
  64. iatoolkit/templates/error.html +44 -8
  65. iatoolkit/templates/forgot_password.html +40 -23
  66. iatoolkit/templates/index.html +145 -0
  67. iatoolkit/templates/login_simulation.html +45 -0
  68. iatoolkit/templates/onboarding_shell.html +107 -0
  69. iatoolkit/templates/signup.html +63 -65
  70. iatoolkit/views/base_login_view.py +91 -0
  71. iatoolkit/views/change_password_view.py +56 -31
  72. iatoolkit/views/embedding_api_view.py +65 -0
  73. iatoolkit/views/external_login_view.py +61 -28
  74. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +10 -3
  75. iatoolkit/views/forgot_password_view.py +27 -21
  76. iatoolkit/views/help_content_api_view.py +54 -0
  77. iatoolkit/views/history_api_view.py +56 -0
  78. iatoolkit/views/home_view.py +50 -23
  79. iatoolkit/views/index_view.py +14 -0
  80. iatoolkit/views/init_context_api_view.py +74 -0
  81. iatoolkit/views/llmquery_api_view.py +58 -0
  82. iatoolkit/views/login_simulation_view.py +93 -0
  83. iatoolkit/views/login_view.py +130 -37
  84. iatoolkit/views/logout_api_view.py +49 -0
  85. iatoolkit/views/profile_api_view.py +46 -0
  86. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
  87. iatoolkit/views/signup_view.py +41 -36
  88. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  89. iatoolkit/views/tasks_review_api_view.py +55 -0
  90. iatoolkit/views/user_feedback_api_view.py +60 -0
  91. iatoolkit/views/verify_user_view.py +34 -29
  92. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/METADATA +41 -23
  93. iatoolkit-0.71.2.dist-info/RECORD +122 -0
  94. iatoolkit-0.71.2.dist-info/licenses/LICENSE +21 -0
  95. iatoolkit/common/auth.py +0 -200
  96. iatoolkit/static/images/arrow_up.png +0 -0
  97. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  98. iatoolkit/static/images/logo_clinica.png +0 -0
  99. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  100. iatoolkit/static/images/logo_maxxa.png +0 -0
  101. iatoolkit/static/images/logo_notaria.png +0 -0
  102. iatoolkit/static/images/logo_tarjeta.png +0 -0
  103. iatoolkit/static/images/logo_umayor.png +0 -0
  104. iatoolkit/static/images/upload.png +0 -0
  105. iatoolkit/static/js/chat_feedback.js +0 -115
  106. iatoolkit/static/js/chat_history.js +0 -117
  107. iatoolkit/static/styles/chat_info.css +0 -53
  108. iatoolkit/templates/header.html +0 -31
  109. iatoolkit/templates/home.html +0 -199
  110. iatoolkit/templates/login.html +0 -43
  111. iatoolkit/templates/test.html +0 -9
  112. iatoolkit/views/chat_token_request_view.py +0 -98
  113. iatoolkit/views/chat_view.py +0 -58
  114. iatoolkit/views/download_file_view.py +0 -58
  115. iatoolkit/views/external_chat_login_view.py +0 -95
  116. iatoolkit/views/history_view.py +0 -57
  117. iatoolkit/views/llmquery_view.py +0 -65
  118. iatoolkit/views/tasks_review_view.py +0 -83
  119. iatoolkit/views/user_feedback_view.py +0 -74
  120. iatoolkit-0.11.0.dist-info/RECORD +0 -110
  121. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/WHEEL +0 -0
  122. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,36 @@
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ const logoutButton = document.getElementById('logout-button');
3
+ if (!logoutButton) {
4
+ console.warn('El botón de logout con id "logout-button" no fue encontrado.');
5
+ return;
6
+ }
7
+
8
+ if (window.toastr) {
9
+ toastr.options = { "positionClass": "toast-bottom-right", "preventDuplicates": true };
10
+ }
11
+
12
+ logoutButton.addEventListener('click', async function(event) {
13
+ event.preventDefault();
14
+
15
+ try {
16
+ const apiPath = '/api/logout';
17
+ const data = await callToolkit(apiPath, null, 'GET');
18
+
19
+ // Procesar la respuesta
20
+ if (data && data.status === 'success' && data.url) {
21
+ window.top.location.href = data.url;
22
+ } else {
23
+ // Si algo falla, callToolkit usualmente muestra un error.
24
+ // Mostramos un toast como fallback.
25
+ if (window.toastr) {
26
+ toastr.error('No se pudo procesar el cierre de sesión. Por favor, intente de nuevo.');
27
+ }
28
+ }
29
+ } catch (error) {
30
+ console.error('Error durante el logout:', error);
31
+ if (window.toastr) {
32
+ toastr.error('Ocurrió un error de red al intentar cerrar sesión.');
33
+ }
34
+ }
35
+ });
36
+ });
@@ -1,38 +1,41 @@
1
1
  // Global variables for request management
2
- let currentAbortController = null;
3
2
  let isRequestInProgress = false;
4
-
3
+ let abortController = null;
5
4
  let selectedPrompt = null; // Will hold a lightweight prompt object
6
5
 
7
6
  $(document).ready(function () {
8
- // --- MAIN EVENT HANDLERS ---
9
- $('#send-button').on('click', handleChatMessage);
10
- $('#stop-button').on('click', abortCurrentRequest);
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
+ }
11
13
 
12
- // --- PROMPT ASSISTANT FUNCTIONALITY ---
13
- $('.input-area').on('click', '.dropdown-menu a.dropdown-item', function (event) {
14
- event.preventDefault();
15
- const promptData = $(this).data();
14
+ const layoutContainer = document.querySelector('.chat-layout-container');
15
+ const promptAssistantCollapse = document.getElementById('prompt-assistant-collapse');
16
16
 
17
- const promptObject = {
18
- prompt: promptData.promptName,
19
- description: promptData.promptDescription,
20
- custom_fields: typeof promptData.customFields === 'string' ? JSON.parse(promptData.customFields) : promptData.customFields
21
- };
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
+ });
22
24
 
23
- selectPrompt(promptObject);
24
- });
25
+ promptAssistantCollapse.addEventListener('hide.bs.collapse', function () {
26
+ layoutContainer.classList.remove('prompt-assistant-open');
27
+ });
28
+ }
25
29
 
26
- // Handles the 'clear' button for the prompt selector
27
- $('#clear-selection-button').on('click', function() {
28
- resetPromptSelection();
29
- updateSendButtonState();
30
- });
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);
31
35
 
32
- // --- TEXTAREA FUNCTIONALITY ---
33
- const questionTextarea = $('#question');
34
36
 
35
37
  // Handles Enter key press to send a message
38
+ const questionTextarea = $('#question');
36
39
  questionTextarea.on('keypress', function (event) {
37
40
  if (event.key === 'Enter' && !event.shiftKey) {
38
41
  event.preventDefault();
@@ -52,141 +55,101 @@ $(document).ready(function () {
52
55
 
53
56
  // Set the initial disabled state of the send button
54
57
  updateSendButtonState();
55
- });
56
-
57
-
58
- /**
59
- * Handles the selection of a prompt from the dropdown.
60
- * @param {object} prompt The prompt object read from data attributes.
61
- */
62
- function selectPrompt(prompt) {
63
- selectedPrompt = prompt;
64
-
65
- // Update the dropdown button to show the selected prompt's description
66
- $('#prompt-select-button').text(prompt.description).addClass('item-selected');
67
- $('#clear-selection-button').show();
68
-
69
- // Clear the main textarea, as we are now in "prompt mode"
70
- $('#question').val('');
71
- autoResizeTextarea($('#question')[0]); // Reset height after clearing
72
-
73
- // Store values in hidden fields for backward compatibility or other uses
74
- $('#prompt-select-value').val(prompt.prompt);
75
- $('#prompt-select-description').val(prompt.description);
76
-
77
- // Render the dynamic input fields required by the selected prompt
78
- renderDynamicInputs(prompt.custom_fields || []);
79
- updateSendButtonState();
80
- }
81
58
 
82
- /**
83
- * Resets the prompt selection and clears associated UI elements.
84
- */
85
- function resetPromptSelection() {
86
- selectedPrompt = null;
87
-
88
- $('#prompt-select-button').text('Prompts disponibles ....').removeClass('item-selected');
89
- $('#clear-selection-button').hide();
90
- $('#prompt-select-value').val('');
91
- $('#prompt-select-description').val('');
92
-
93
- // Clear any dynamically generated input fields
94
- $('#dynamic-inputs-container').empty();
95
- }
96
-
97
- /**
98
- * Renders the custom input fields for the selected prompt.
99
- * @param {Array<object>} fields The array of custom field configurations.
100
- */
101
- function renderDynamicInputs(fields) {
102
- const container = $('#dynamic-inputs-container');
103
- container.empty();
104
-
105
- const row = $('<div class="row g-2"></div>');
106
- fields.forEach(field => {
107
- const colDiv = $('<div class="col-md"></div>');
108
- const formFloating = $('<div class="form-floating"></div>');
109
- const input = $(`<input type="${field.type || 'text'}" class="form-control form-control-soft" id="${field.data_key}-id" ">`);
110
- const label = $(`<label for="${field.data_key}-id">${field.label}</label>`);
111
-
112
- formFloating.append(input, label);
113
- colDiv.append(formFloating);
114
- row.append(colDiv);
115
- });
116
-
117
- container.append(row);
118
- }
59
+ });
119
60
 
120
61
 
121
62
  /**
122
63
  * Main function to handle sending a chat message.
123
64
  */
124
65
  const handleChatMessage = async function () {
125
- if (isRequestInProgress || $('#send-button').hasClass('disabled')) return;
126
-
127
- const question = $('#question').val().trim();
128
- const promptName = selectedPrompt ? selectedPrompt.prompt : null;
66
+ if (isRequestInProgress || $('#send-button').hasClass('disabled')) {
67
+ return;
68
+ }
129
69
 
130
- let displayMessage = question;
131
- let isEditable = true;
132
- const clientData = {};
70
+ isRequestInProgress = true;
71
+ toggleSendStopButtons(true);
133
72
 
134
- if (selectedPrompt) {
135
- displayMessage = selectedPrompt.description;
136
- isEditable = false; // Prompts are not editable
73
+ try {
74
+ const question = $('#question').val().trim();
75
+ const promptName = selectedPrompt ? selectedPrompt.prompt : null;
137
76
 
138
- (selectedPrompt.custom_fields || []).forEach(field => {
139
- const value = $('#' + field.data_key + '-id').val().trim();
140
- if (value) {
141
- clientData[field.data_key] = value;
142
- }
143
- });
77
+ let displayMessage = question;
78
+ let isEditable = true;
79
+ const clientData = {};
144
80
 
145
- // Append the collected parameter values to the display message
146
- const paramsString = Object.values(clientData).join(', ');
147
- if (paramsString) {
148
- displayMessage += `: ${paramsString}`;
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}`; }
149
94
  }
150
- }
151
95
 
152
- // Si no hay pregunta libre Y no se ha seleccionado un prompt, no hacer nada.
153
- if (!displayMessage) return;
154
-
155
- displayUserMessage(displayMessage, isEditable, question);
156
- showSpinner();
157
- toggleSendStopButtons(true);
96
+ // Simplificado: Si no hay mensaje, el 'finally' se encargará de limpiar.
97
+ if (!displayMessage) {
98
+ return;
99
+ }
158
100
 
159
- resetAllInputs();
101
+ displayUserMessage(displayMessage, isEditable, question);
102
+ showSpinner();
103
+ resetAllInputs();
160
104
 
161
- const files = window.filePond.getFiles();
162
- const filesBase64 = await Promise.all(files.map(fileItem => toBase64(fileItem.file)));
105
+ const files = window.filePond.getFiles();
106
+ const filesBase64 = await Promise.all(files.map(fileItem => toBase64(fileItem.file)));
163
107
 
164
- // Prepare data payload
165
- const data = {
166
- question: question,
167
- prompt_name: promptName,
168
- client_data: clientData,
169
- files: filesBase64.map(f => ({ filename: f.name, content: f.base64 })),
170
- external_user_id: window.externalUserId
171
- };
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
+ };
172
115
 
173
- try {
174
- const responseData = await callLLMAPI("/llm_query", data, "POST");
116
+ const responseData = await callToolkit("/api/llm_query", data, "POST");
175
117
  if (responseData && responseData.answer) {
176
118
  const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
177
119
  displayBotMessage(answerSection);
178
120
  }
179
121
  } catch (error) {
180
- console.error("Error in handleChatMessage:", error);
181
- // Implement error display logic as needed
122
+ if (error.name === 'AbortError') {
123
+ console.log('Petición abortada por el usuario.');
124
+
125
+ // Usando jQuery estándar para construir el elemento ---
126
+ const icon = $('<i>').addClass('bi bi-stop-circle me-2'); // Icono sin "fill" para un look más ligero
127
+ const textSpan = $('<span>').text('La generación de la respuesta ha sido detenida.');
128
+
129
+ const abortMessage = $('<div>')
130
+ .addClass('system-message')
131
+ .append(icon)
132
+ .append(textSpan);
133
+
134
+ displayBotMessage(abortMessage);
135
+ } else {
136
+ console.error("Error in handleChatMessage:", error);
137
+ const errorSection = $('<div>').addClass('error-section').append('<p>Ocurrió un error al procesar la solicitud.</p>');
138
+ displayBotMessage(errorSection);
139
+ }
182
140
  } finally {
141
+ // Este bloque se ejecuta siempre, garantizando que el estado se limpie.
142
+ isRequestInProgress = false;
183
143
  hideSpinner();
184
144
  toggleSendStopButtons(false);
185
145
  updateSendButtonState();
186
- window.filePond.removeFiles();
146
+ if (window.filePond) {
147
+ window.filePond.removeFiles();
148
+ }
187
149
  }
188
150
  };
189
151
 
152
+
190
153
  /**
191
154
  * Resets all inputs to their initial state.
192
155
  */
@@ -236,28 +199,6 @@ const toggleSendStopButtons = function (showStop) {
236
199
  $('#stop-button-container').toggle(showStop);
237
200
  };
238
201
 
239
- /**
240
- * Resets the prompt selector to its default state.
241
- */
242
- function resetPromptSelect() {
243
- $('#prompt-select-button').text('Prompts disponibles ....').removeClass('item-selected');
244
- $('#prompt-select-value').val('');
245
- $('#prompt-select-description').val('');
246
- $('#clear-selection-button').hide();
247
- }
248
-
249
- /**
250
- * Resets the company-specific data input field.
251
- */
252
- function resetSpecificDataInput() {
253
- if (specificDataConfig && specificDataConfig.enabled) {
254
- const input = $('#' + specificDataConfig.id);
255
- input.val('').removeClass('has-content');
256
- $('#clear-' + specificDataConfig.id + '-button').hide();
257
- }
258
- }
259
-
260
-
261
202
  /**
262
203
  * Generic function to make API calls to the backend.
263
204
  * @param {string} apiPath - The API endpoint path.
@@ -266,32 +207,43 @@ function resetSpecificDataInput() {
266
207
  * @param {number} timeoutMs - Timeout in milliseconds.
267
208
  * @returns {Promise<object|null>} The response data or null on error.
268
209
  */
269
- const callLLMAPI = async function(apiPath, data, method, timeoutMs = 500000) {
210
+ const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
270
211
  const url = `${window.iatoolkit_base_url}/${window.companyShortName}${apiPath}`;
271
212
 
272
- const headers = {"Content-Type": "application/json"};
273
- if (window.sessionJWT) {
274
- headers['X-Chat-Token'] = window.sessionJWT;
275
- }
276
-
277
- const controller = new AbortController();
278
- currentAbortController = controller;
279
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
213
+ abortController = new AbortController();
214
+ const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
280
215
 
281
216
  try {
282
- const response = await fetch(url, {
283
- method: method,
284
- headers: headers,
285
- body: JSON.stringify(data),
286
- signal: controller.signal,
287
- credentials: 'include'
288
- });
289
- clearTimeout(timeoutId);
217
+ const fetchOptions = {
218
+ method: method,
219
+ signal: abortController.signal,
220
+ credentials: 'include'
221
+ };
222
+
223
+ // Solo agrega body si el método lo soporta y hay datos
224
+ const methodUpper = (method || '').toUpperCase();
225
+ const canHaveBody = !['GET', 'HEAD'].includes(methodUpper);
226
+ if (canHaveBody && data !== undefined && data !== null) {
227
+ fetchOptions.body = JSON.stringify(data);
228
+ fetchOptions.headers = {"Content-Type": "application/json"};
290
229
 
230
+ }
231
+ const response = await fetch(url, fetchOptions);
232
+ clearTimeout(timeoutId);
291
233
  if (!response.ok) {
292
- const errorData = await response.json();
293
- const endpointError = $('<div>').addClass('error-section').append(`<p>${errorData.error_message || 'Unknown server error'}</p>`);
294
- displayBotMessage(endpointError);
234
+ try {
235
+ // Intentamos leer el error como JSON, que es el formato esperado de nuestra API.
236
+ const errorData = await response.json();
237
+ const errorMessage = errorData.error_message || t_js('unknown_server_error'); // <-- Translation
238
+ const errorIcon = '<i class="bi bi-exclamation-triangle"></i>';
239
+ const endpointError = $('<div>').addClass('error-section').html(errorIcon + `<p>${errorMessage}</p>`);
240
+ displayBotMessage(endpointError);
241
+ } catch (e) {
242
+ // Si response.json() falla, es porque el cuerpo no era JSON (ej. un 502 con HTML).
243
+ // Mostramos un error genérico y más claro para el usuario.
244
+ const errorMessage = `Error de comunicación con el servidor (${response.status}). Por favor, intente de nuevo más tarde.`;
245
+ toastr.error(errorMessage);
246
+ }
295
247
  return null;
296
248
  }
297
249
  return await response.json();
@@ -300,8 +252,7 @@ const callLLMAPI = async function(apiPath, data, method, timeoutMs = 500000) {
300
252
  if (error.name === 'AbortError') {
301
253
  throw error; // Re-throw to be handled by handleChatMessage
302
254
  } else {
303
- const commError = $('<div>').addClass('error-section').append(`<p>Connection error: ${error.message}</p>`);
304
- displayBotMessage(commError);
255
+ toastr.error(t_js('network_error') );
305
256
  }
306
257
  return null;
307
258
  }
@@ -322,10 +273,13 @@ const displayUserMessage = function(message, isEditable, originalQuestion) {
322
273
  userMessage.append(messageText);
323
274
 
324
275
  if (isEditable) {
325
- const editIcon = $('<i>').addClass('p-2 bi bi-pencil-fill edit-icon').attr('title', 'Edit query').on('click', function () {
326
- $('#question').val(originalQuestion).focus();
276
+ const editIcon = $('<i>').addClass('p-2 bi bi-pencil-fill edit-icon edit-pencil').attr('title', 'Edit query').on('click', function () {
277
+ $('#question').val(originalQuestion)
327
278
  autoResizeTextarea($('#question')[0]);
328
- updateSendButtonState();
279
+ $('#send-button').removeClass('disabled');
280
+
281
+ if (window.innerWidth > 768)
282
+ $('#question').focus();
329
283
  });
330
284
  userMessage.append(editIcon);
331
285
  }
@@ -347,9 +301,8 @@ function displayBotMessage(section) {
347
301
  * Aborts the current in-progress API request.
348
302
  */
349
303
  const abortCurrentRequest = function () {
350
- if (currentAbortController && isRequestInProgress) {
351
- window.isManualAbort = true;
352
- currentAbortController.abort();
304
+ if (isRequestInProgress && abortController) {
305
+ abortController.abort();
353
306
  }
354
307
  };
355
308
 
@@ -359,12 +312,13 @@ const abortCurrentRequest = function () {
359
312
  const showSpinner = function () {
360
313
  if ($('#spinner').length) return;
361
314
  const accessibilityClass = (typeof bootstrap !== 'undefined') ? 'visually-hidden' : 'sr-only';
315
+ const spinnerText = t_js('loading');
362
316
  const spinner = $(`
363
317
  <div id="spinner" style="display: flex; align-items: center; justify-content: start; margin: 10px 0; padding: 10px;">
364
- <div class="spinner-border text-primary" role="status" style="width: 1.5rem; height: 1.5rem; margin-right: 15px;">
318
+ <div class="spinner-border" role="status" style="width: 1.5rem; height: 1.5rem; margin-right: 15px;">
365
319
  <span class="${accessibilityClass}">Loading...</span>
366
320
  </div>
367
- <span style="font-weight: bold; font-size: 15px;">Loading...</span>
321
+ <span style="font-weight: bold; font-size: 15px;">${spinnerText}</span>
368
322
  </div>
369
323
  `);
370
324
  $('#chat-container').append(spinner).scrollTop($('#chat-container')[0].scrollHeight);
@@ -393,44 +347,3 @@ function toBase64(file) {
393
347
  });
394
348
  }
395
349
 
396
- /**
397
- * Displays the document validation results.
398
- * @param {Array<object>} document_list
399
- */
400
- function display_document_validation(document_list) {
401
- const requiredFields = ['document_name', 'document_type', 'causes', 'is_valid'];
402
- for (const doc of document_list) {
403
- if (!requiredFields.every(field => field in doc)) {
404
- console.warn("Document with incorrect structure:", doc);
405
- continue;
406
- }
407
- const docValidationSection = $('<div>').addClass('document-section card mt-2 mb-2');
408
- const cardBody = $('<div>').addClass('card-body');
409
- const headerDiv = $('<div>').addClass('d-flex justify-content-between align-items-center mb-2');
410
- const filenameSpan = $(`
411
- <div>
412
- <span class="text-primary fw-bold">File: </span>
413
- <span class="text-secondary">${doc.document_name}</span>
414
- </div>`);
415
- const badge_style = doc.is_valid ? 'bg-success' : 'bg-danger';
416
- const documentBadge = $('<span>')
417
- .addClass(`badge ${badge_style} p-2`)
418
- .text(doc.document_type);
419
- headerDiv.append(filenameSpan).append(documentBadge);
420
- cardBody.append(headerDiv);
421
-
422
- if (!doc.is_valid && doc.causes && doc.causes.length > 0) {
423
- const rejectionSection = $('<div>').addClass('rejection-reasons mt-2');
424
- rejectionSection.append('<h6 class="text-danger">Rejection Causes:</h6>');
425
- const causesList = doc.causes.map(cause => `<li class="text-secondary">${cause}</li>`).join('');
426
- rejectionSection.append(`<ul class="list-unstyled">${causesList}</ul>`);
427
- cardBody.append(rejectionSection);
428
- } else if (doc.is_valid) {
429
- const validSection = $('<div>').addClass('mt-2');
430
- validSection.append('<p class="text-success fw-bold">Valid document.</p>');
431
- cardBody.append(validSection);
432
- }
433
- docValidationSection.append(cardBody);
434
- displayBotMessage(docValidationSection);
435
- }
436
- }
@@ -0,0 +1,103 @@
1
+ (function (global) {
2
+ function qs(root, sel) { return (typeof sel === 'string') ? root.querySelector(sel) : sel; }
3
+
4
+ function createDots(container, count, activeIdx, activeColor) {
5
+ container.innerHTML = '';
6
+ for (let i = 0; i < count; i++) {
7
+ const d = document.createElement('div');
8
+ if (i === activeIdx) d.classList.add('active');
9
+ d.style.width = '10px';
10
+ d.style.height = '10px';
11
+ d.style.borderRadius = '50%';
12
+ d.style.backgroundColor = i === activeIdx ? (activeColor || 'var(--brand-primary-color, #FF5100)') : '#ddd';
13
+ d.style.transition = 'background-color .3s';
14
+ container.appendChild(d);
15
+ }
16
+ }
17
+
18
+ function initOnboarding(opts) {
19
+ const {
20
+ mode = 'modal',
21
+ cards = [],
22
+ ui = {},
23
+ autoRotateMs = 5000,
24
+ shell = {}
25
+ } = opts;
26
+
27
+ const root = document;
28
+ const elIcon = qs(root, ui.icon);
29
+ const elTitle = qs(root, ui.title);
30
+ const elText = qs(root, ui.text);
31
+ const elExample = qs(root, ui.example);
32
+ const elDots = qs(root, ui.dots);
33
+ const elPrev = qs(root, ui.prev);
34
+ const elNext = qs(root, ui.next);
35
+
36
+ let idx = 0;
37
+ let autoTimer = null;
38
+
39
+ function hasCards() { return Array.isArray(cards) && cards.length > 0; }
40
+
41
+ function render() {
42
+ if (!hasCards()) return;
43
+ const c = cards[idx] || {};
44
+ if (elIcon) elIcon.innerHTML = `<i class="${c.icon || 'bi bi-lightbulb'}"></i>`;
45
+ if (elTitle) elTitle.textContent = c.title || '';
46
+ if (elText) elText.innerHTML = c.text || '';
47
+ if (elExample && c.example) {
48
+ elExample.innerHTML = ('Ejemplo: ' + c.example) || '';
49
+ }
50
+ else
51
+ elExample.innerHTML = '';
52
+ if (elDots) createDots(elDots, cards.length, idx);
53
+ }
54
+
55
+ function next() { if (!hasCards()) return; idx = (idx + 1) % cards.length; render(); }
56
+ function prev() { if (!hasCards()) return; idx = (idx - 1 + cards.length) % cards.length; render(); }
57
+
58
+ function startAuto() {
59
+ stopAuto();
60
+ if (!hasCards()) return;
61
+ autoTimer = setInterval(next, autoRotateMs);
62
+ }
63
+ function stopAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
64
+
65
+ function setupShellIfNeeded() {
66
+ if (mode !== 'shell') return;
67
+ const loader = ui.loader ? qs(root, ui.loader) : null;
68
+ const container = ui.container ? qs(root, ui.container) : null;
69
+ if (!container || !shell.iframeSrc) return;
70
+
71
+ const iframe = document.createElement('iframe');
72
+ iframe.src = shell.iframeSrc;
73
+ iframe.style.width = '100%';
74
+ iframe.style.height = '100%';
75
+ iframe.style.border = 'none';
76
+ iframe.style.display = 'none';
77
+
78
+ iframe.onload = function () {
79
+ iframe.style.display = 'block';
80
+ if (loader) {
81
+ loader.style.opacity = '0';
82
+ setTimeout(() => loader.style.display = 'none', 500);
83
+ }
84
+ };
85
+ container.appendChild(iframe);
86
+ }
87
+
88
+ if (elPrev) elPrev.addEventListener('click', () => { prev(); startAuto(); });
89
+ if (elNext) elNext.addEventListener('click', () => { next(); startAuto(); });
90
+
91
+ function start() {
92
+ idx = 0;
93
+ render();
94
+ startAuto();
95
+ if (mode === 'shell') setupShellIfNeeded();
96
+ }
97
+
98
+ return { start, stop: stopAuto, next, prev, hasCards };
99
+ }
100
+
101
+ // Export global
102
+ global.initOnboarding = initOnboarding;
103
+ })(window);