iatoolkit 0.63.1__py3-none-any.whl → 0.69.0__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 (83) hide show
  1. iatoolkit/__init__.py +0 -2
  2. iatoolkit/base_company.py +1 -26
  3. iatoolkit/common/routes.py +11 -2
  4. iatoolkit/common/session_manager.py +2 -0
  5. iatoolkit/common/util.py +17 -0
  6. iatoolkit/company_registry.py +1 -2
  7. iatoolkit/iatoolkit.py +39 -6
  8. iatoolkit/locales/en.yaml +167 -0
  9. iatoolkit/locales/es.yaml +163 -0
  10. iatoolkit/repositories/database_manager.py +8 -3
  11. iatoolkit/repositories/document_repo.py +1 -1
  12. iatoolkit/repositories/models.py +1 -4
  13. iatoolkit/repositories/profile_repo.py +0 -4
  14. iatoolkit/services/auth_service.py +14 -9
  15. iatoolkit/services/branding_service.py +36 -24
  16. iatoolkit/services/company_context_service.py +145 -0
  17. iatoolkit/services/configuration_service.py +133 -0
  18. iatoolkit/services/dispatcher_service.py +51 -48
  19. iatoolkit/services/document_service.py +5 -2
  20. iatoolkit/services/excel_service.py +15 -11
  21. iatoolkit/services/file_processor_service.py +4 -12
  22. iatoolkit/services/history_service.py +8 -7
  23. iatoolkit/services/i18n_service.py +104 -0
  24. iatoolkit/services/jwt_service.py +7 -9
  25. iatoolkit/services/language_service.py +83 -0
  26. iatoolkit/services/load_documents_service.py +4 -4
  27. iatoolkit/services/mail_service.py +9 -4
  28. iatoolkit/services/profile_service.py +61 -38
  29. iatoolkit/services/prompt_manager_service.py +20 -16
  30. iatoolkit/services/query_service.py +19 -15
  31. iatoolkit/services/search_service.py +11 -4
  32. iatoolkit/services/sql_service.py +55 -25
  33. iatoolkit/services/user_feedback_service.py +16 -14
  34. iatoolkit/static/js/chat_feedback_button.js +57 -87
  35. iatoolkit/static/js/chat_help_content.js +124 -0
  36. iatoolkit/static/js/chat_history_button.js +48 -65
  37. iatoolkit/static/js/chat_main.js +27 -24
  38. iatoolkit/static/js/chat_onboarding_button.js +6 -0
  39. iatoolkit/static/js/chat_reload_button.js +28 -45
  40. iatoolkit/static/styles/chat_iatoolkit.css +223 -315
  41. iatoolkit/static/styles/chat_modal.css +63 -97
  42. iatoolkit/static/styles/chat_public.css +107 -0
  43. iatoolkit/static/styles/landing_page.css +0 -1
  44. iatoolkit/static/styles/onboarding.css +7 -0
  45. iatoolkit/templates/_company_header.html +6 -2
  46. iatoolkit/templates/_login_widget.html +42 -0
  47. iatoolkit/templates/base.html +34 -19
  48. iatoolkit/templates/change_password.html +22 -20
  49. iatoolkit/templates/chat.html +59 -27
  50. iatoolkit/templates/chat_modals.html +114 -74
  51. iatoolkit/templates/error.html +12 -13
  52. iatoolkit/templates/forgot_password.html +11 -7
  53. iatoolkit/templates/index.html +8 -3
  54. iatoolkit/templates/login_simulation.html +17 -6
  55. iatoolkit/templates/onboarding_shell.html +4 -2
  56. iatoolkit/templates/signup.html +14 -14
  57. iatoolkit/views/base_login_view.py +19 -9
  58. iatoolkit/views/change_password_view.py +50 -35
  59. iatoolkit/views/external_login_view.py +1 -1
  60. iatoolkit/views/forgot_password_view.py +21 -22
  61. iatoolkit/views/help_content_api_view.py +54 -0
  62. iatoolkit/views/history_api_view.py +13 -9
  63. iatoolkit/views/home_view.py +30 -39
  64. iatoolkit/views/init_context_api_view.py +16 -11
  65. iatoolkit/views/llmquery_api_view.py +38 -26
  66. iatoolkit/views/login_simulation_view.py +14 -2
  67. iatoolkit/views/login_view.py +52 -40
  68. iatoolkit/views/logout_api_view.py +26 -22
  69. iatoolkit/views/profile_api_view.py +46 -0
  70. iatoolkit/views/prompt_api_view.py +6 -6
  71. iatoolkit/views/signup_view.py +27 -27
  72. iatoolkit/views/user_feedback_api_view.py +19 -18
  73. iatoolkit/views/verify_user_view.py +29 -30
  74. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/METADATA +40 -22
  75. iatoolkit-0.69.0.dist-info/RECORD +120 -0
  76. iatoolkit-0.69.0.dist-info/licenses/LICENSE +21 -0
  77. iatoolkit/services/onboarding_service.py +0 -43
  78. iatoolkit/static/styles/chat_info.css +0 -53
  79. iatoolkit/templates/header.html +0 -31
  80. iatoolkit/templates/test.html +0 -9
  81. iatoolkit-0.63.1.dist-info/RECORD +0 -112
  82. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/WHEEL +0 -0
  83. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/top_level.txt +0 -0
@@ -4,57 +4,87 @@
4
4
  # IAToolkit is open source software.
5
5
 
6
6
  from iatoolkit.repositories.database_manager import DatabaseManager
7
-
8
7
  from iatoolkit.common.util import Utility
8
+ from iatoolkit.services.i18n_service import I18nService
9
+ from iatoolkit.common.exceptions import IAToolkitException
9
10
  from sqlalchemy import text
10
- from injector import inject
11
+ from injector import inject, singleton
11
12
  import json
12
- from iatoolkit.common.exceptions import IAToolkitException
13
+ import logging
13
14
 
14
15
 
16
+ @singleton
15
17
  class SqlService:
18
+ """
19
+ Manages database connections and executes SQL statements.
20
+ It maintains a cache of named DatabaseManager instances to avoid reconnecting.
21
+ """
22
+
16
23
  @inject
17
- def __init__(self,util: Utility):
24
+ def __init__(self,
25
+ util: Utility,
26
+ i18n_service: I18nService):
18
27
  self.util = util
28
+ self.i18n_service = i18n_service
19
29
 
20
- def exec_sql(self, db_manager: DatabaseManager, sql_statement: str) -> str:
30
+ # Cache for database connections
31
+ self._db_connections: dict[str, DatabaseManager] = {}
32
+
33
+ def register_database(self, db_name: str, db_uri: str):
34
+ """
35
+ Creates and caches a DatabaseManager instance for a given database name and URI.
36
+ If a database with the same name is already registered, it does nothing.
21
37
  """
22
- Executes a raw SQL statement and returns the result as a JSON string.
38
+ if db_name in self._db_connections:
39
+ return
23
40
 
24
- This method takes a DatabaseManager instance and a SQL query, executes it
25
- against the database, and fetches all results. The results are converted
26
- into a list of dictionaries, where each dictionary represents a row.
27
- This list is then serialized to a JSON string.
28
- If an exception occurs during execution, the transaction is rolled back,
29
- and a custom IAToolkitException is raised.
41
+ logging.debug(f"Registering and creating connection for database: '{db_name}'")
42
+ db_manager = DatabaseManager(db_uri, register_pgvector=False)
43
+ self._db_connections[db_name] = db_manager
30
44
 
31
- Args:
32
- db_manager: The DatabaseManager instance to get the database session from.
33
- sql_statement: The raw SQL statement to be executed.
45
+ def get_database_manager(self, db_name: str) -> DatabaseManager:
46
+ """
47
+ Retrieves a registered DatabaseManager instance from the cache.
48
+ """
49
+ try:
50
+ return self._db_connections[db_name]
51
+ except KeyError:
52
+ logging.error(f"Attempted to access unregistered database: '{db_name}'")
53
+ raise IAToolkitException(
54
+ IAToolkitException.ErrorType.DATABASE_ERROR,
55
+ f"Database '{db_name}' is not registered with the SqlService."
56
+ )
34
57
 
35
- Returns:
36
- A JSON string representing the list of rows returned by the query.
58
+ def exec_sql(self, db_name: str, sql_statement: str) -> str:
59
+ """
60
+ Executes a raw SQL statement against a registered database and returns the result as a JSON string.
37
61
  """
38
62
  try:
39
- # here the SQL is executed
40
- result = db_manager.get_session().execute(text(sql_statement))
63
+ # 1. Get the database manager from the cache
64
+ db_manager = self.get_database_manager(db_name)
41
65
 
42
- # get the column names
66
+ # 2. Execute the SQL statement
67
+ result = db_manager.get_session().execute(text(sql_statement))
43
68
  cols = result.keys()
44
-
45
- # convert rows to dict
46
69
  rows_context = [dict(zip(cols, row)) for row in result.fetchall()]
47
70
 
48
- # Serialize to JSON with type convertion
71
+ # seialize the result
49
72
  sql_result_json = json.dumps(rows_context, default=self.util.serialize)
50
73
 
51
74
  return sql_result_json
75
+ except IAToolkitException:
76
+ # Re-raise exceptions from get_database_manager to preserve the specific error
77
+ raise
52
78
  except Exception as e:
53
- db_manager.get_session().rollback()
79
+ # Attempt to rollback if a session was active
80
+ db_manager = self._db_connections.get(db_name)
81
+ if db_manager:
82
+ db_manager.get_session().rollback()
54
83
 
55
84
  error_message = str(e)
56
85
  if 'timed out' in str(e):
57
- error_message = 'Intentalo de nuevo, se agoto el tiempo de espera'
86
+ error_message = self.i18n_service.t('errors.timeout')
58
87
 
88
+ logging.error(f"Error executing SQL statement: {error_message}")
59
89
  raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
60
90
  error_message) from e
@@ -6,8 +6,9 @@
6
6
  from iatoolkit.repositories.models import UserFeedback, Company
7
7
  from injector import inject
8
8
  from iatoolkit.repositories.profile_repo import ProfileRepo
9
+ from iatoolkit.services.i18n_service import I18nService
9
10
  from iatoolkit.infra.google_chat_app import GoogleChatApp
10
- from iatoolkit.infra.mail_app import MailApp # <-- 1. Importar MailApp
11
+ from iatoolkit.infra.mail_app import MailApp
11
12
  import logging
12
13
 
13
14
 
@@ -15,9 +16,11 @@ class UserFeedbackService:
15
16
  @inject
16
17
  def __init__(self,
17
18
  profile_repo: ProfileRepo,
19
+ i18n_service: I18nService,
18
20
  google_chat_app: GoogleChatApp,
19
21
  mail_app: MailApp):
20
22
  self.profile_repo = profile_repo
23
+ self.i18n_service = i18n_service
21
24
  self.google_chat_app = google_chat_app
22
25
  self.mail_app = mail_app
23
26
 
@@ -31,9 +34,9 @@ class UserFeedbackService:
31
34
  }
32
35
  chat_result = self.google_chat_app.send_message(message_data=chat_data)
33
36
  if not chat_result.get('success'):
34
- logging.warning(f"Error al enviar notificación a Google Chat: {chat_result.get('message')}")
37
+ logging.warning(f"error sending notification to Google Chat: {chat_result.get('message')}")
35
38
  except Exception as e:
36
- logging.exception(f"Fallo inesperado al enviar notificación a Google Chat: {e}")
39
+ logging.exception(f"error sending notification to Google Chat: {e}")
37
40
 
38
41
  def _send_email_notification(self, destination_email: str, company_name: str, message_text: str):
39
42
  """Envía una notificación de feedback por correo electrónico."""
@@ -43,20 +46,20 @@ class UserFeedbackService:
43
46
  html_body = message_text.replace('\n', '<br>')
44
47
  self.mail_app.send_email(to=destination_email, subject=subject, body=html_body)
45
48
  except Exception as e:
46
- logging.exception(f"Fallo inesperado al enviar email de feedback: {e}")
49
+ logging.exception(f"error sending email de feedback: {e}")
47
50
 
48
51
  def _handle_notification(self, company: Company, message_text: str):
49
52
  """Lee la configuración de la empresa y envía la notificación al canal correspondiente."""
50
53
  feedback_params = company.parameters.get('user_feedback')
51
54
  if not isinstance(feedback_params, dict):
52
- logging.warning(f"No se encontró configuración de 'user_feedback' para la empresa {company.short_name}.")
55
+ logging.warning(f"missing 'user_feedback' configuration for company: {company.short_name}.")
53
56
  return
54
57
 
55
58
  # get channel and destination
56
59
  channel = feedback_params.get('channel')
57
60
  destination = feedback_params.get('destination')
58
61
  if not channel or not destination:
59
- logging.warning(f"Configuración 'user_feedback' incompleta para {company.short_name}. Faltan 'channel' o 'destination'.")
62
+ logging.warning(f"invalid 'user_feedback' configuration for: {company.short_name}. Faltan 'channel' o 'destination'.")
60
63
  return
61
64
 
62
65
  if channel == 'google_chat':
@@ -64,7 +67,7 @@ class UserFeedbackService:
64
67
  elif channel == 'email':
65
68
  self._send_email_notification(destination_email=destination, company_name=company.short_name, message_text=message_text)
66
69
  else:
67
- logging.warning(f"Canal de feedback '{channel}' no reconocido para la empresa {company.short_name}.")
70
+ logging.warning(f"unknown feedback channel: '{channel}' for company {company.short_name}.")
68
71
 
69
72
  def new_feedback(self,
70
73
  company_short_name: str,
@@ -72,19 +75,18 @@ class UserFeedbackService:
72
75
  user_identifier: str,
73
76
  rating: int = None) -> dict:
74
77
  try:
75
- # 1. Validar empresa
76
78
  company = self.profile_repo.get_company_by_short_name(company_short_name)
77
79
  if not company:
78
- return {'error': f'No existe la empresa: {company_short_name}'}
80
+ return {'error': self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
79
81
 
80
- # 2. Enviar notificación según la configuración de la empresa
82
+ # 2. send notification using company configuration
81
83
  notification_text = (f"*Nuevo feedback de {company_short_name}*:\n"
82
84
  f"*Usuario:* {user_identifier}\n"
83
85
  f"*Mensaje:* {message}\n"
84
86
  f"*Calificación:* {rating if rating is not None else 'N/A'}")
85
87
  self._handle_notification(company, notification_text)
86
88
 
87
- # 3. Guardar el feedback en la base de datos (independientemente del éxito de la notificación)
89
+ # 3. always save the feedback in the database
88
90
  new_feedback_obj = UserFeedback(
89
91
  company_id=company.id,
90
92
  message=message,
@@ -93,10 +95,10 @@ class UserFeedbackService:
93
95
  )
94
96
  saved_feedback = self.profile_repo.save_feedback(new_feedback_obj)
95
97
  if not saved_feedback:
96
- logging.error(f"No se pudo guardar el feedback para el usuario {user_identifier} en la empresa {company_short_name}")
97
- return {'error': 'No se pudo guardar el feedback'}
98
+ logging.error(f"can't save feedback for user {user_identifier}/{company_short_name}")
99
+ return {'error': 'can not save the feedback'}
98
100
 
99
- return {'message': 'Feedback guardado correctamente'}
101
+ return {'success': True, 'message': 'Feedback guardado correctamente'}
100
102
 
101
103
  except Exception as e:
102
104
  logging.exception(f"Error crítico en el servicio de feedback: {e}")
@@ -1,110 +1,80 @@
1
1
  $(document).ready(function () {
2
+ const feedbackModal = $('#feedbackModal');
3
+ $('#submit-feedback').on('click', function () {
4
+ sendFeedback(this);
5
+ });
2
6
 
3
7
  // Evento para enviar el feedback
4
- $('#submit-feedback').on('click', async function() {
8
+ async function sendFeedback(submitButton) {
9
+ toastr.options = {"positionClass": "toast-bottom-right", "preventDuplicates": true};
5
10
  const feedbackText = $('#feedback-text').val().trim();
6
- const submitButton = $(this);
7
-
8
- // --- LÓGICA DE COMPATIBILIDAD BS3 / BS5 ---
9
- // Detecta si Bootstrap 5 está presente.
10
- const isBootstrap5 = (typeof bootstrap !== 'undefined');
11
-
12
- // Define el HTML del botón de cierre según la versión.
13
- const closeButtonHtml = isBootstrap5 ?
14
- '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' : // Versión BS5
15
- '<button type="button" class="close" data-dismiss="alert"><span>&times;</span></button>'; // Versión BS3/BS4
16
- // --- FIN DE LA LÓGICA DE COMPATIBILIDAD ---
11
+ const activeStars = $('.star.active').length;
17
12
 
18
13
  if (!feedbackText) {
19
- const alertHtml = `
20
- <div class="alert alert-warning alert-dismissible fade show" role="alert">
21
- <strong>¡Atención!</strong> Por favor, escribe tu comentario antes de enviar.
22
- ${closeButtonHtml}
23
- </div>`;
24
- $('.modal-body .alert').remove();
25
- $('.modal-body').prepend(alertHtml);
14
+ toastr.error(t_js('feedback_comment_error'));
26
15
  return;
27
16
  }
28
17
 
29
- const activeStars = $('.star.active').length;
30
18
  if (activeStars === 0) {
31
- const alertHtml = `
32
- <div class="alert alert-warning alert-dismissible fade show" role="alert">
33
- <strong>¡Atención!</strong> Por favor, califica al asistente con las estrellas.
34
- ${closeButtonHtml}
35
- </div>`;
36
- $('.modal-body .alert').remove();
37
- $('.modal-body').prepend(alertHtml);
19
+ toastr.error(t_js('feedback_rating_error'));
38
20
  return;
39
21
  }
40
22
 
41
- submitButton.prop('disabled', true);
42
- submitButton.html('<i class="bi bi-send me-1 icon-spaced"></i>Enviando...');
23
+ submitButton.disabled = true;
43
24
 
44
- const response = await sendFeedback(feedbackText);
45
- $('#feedbackModal').modal('hide');
46
- if (response)
47
- toastr.success('¡Gracias por tu comentario!', 'Feedback Enviado');
25
+ // call the IAToolkit API to send feedback
26
+ const data = {
27
+ "user_identifier": window.user_identifier,
28
+ "message": feedbackText,
29
+ "rating": activeStars,
30
+ };
31
+
32
+ const responseData = await callToolkit('/api/feedback', data, "POST");
33
+ if (responseData)
34
+ toastr.success(t_js('feedback_sent_success_body'), t_js('feedback_sent_success_title'));
48
35
  else
49
- toastr.error('No se pudo enviar el feedback, por favor intente nuevamente.', 'Error');
50
- });
36
+ toastr.error(t_js('feedback_sent_error'));
51
37
 
52
- // Evento para abrir el modal de feedback
53
- $('#send-feedback-button').on('click', function() {
54
- $('#submit-feedback').prop('disabled', false);
55
- $('#submit-feedback').html('<i class="bi bi-send me-1 icon-spaced"></i>Enviar');
56
- $('.star').removeClass('active hover-active'); // Resetea estrellas
57
- $('#feedback-text').val(''); // Limpia texto
58
- $('.modal-body .alert').remove(); // Quita alertas previas
59
- $('#feedbackModal').modal('show');
60
- });
38
+ submitButton.disabled = false;
39
+ feedbackModal.modal('hide');
40
+ }
61
41
 
62
- // Evento que se dispara DESPUÉS de que el modal se ha ocultado
63
- $('#feedbackModal').on('hidden.bs.modal', function () {
64
- $('#feedback-text').val('');
65
- $('.modal-body .alert').remove();
66
- $('.star').removeClass('active');
42
+ // Evento para abrir el modal de feedback
43
+ $('#send-feedback-button').on('click', function () {
44
+ $('#submit-feedback').prop('disabled', false);
45
+ $('.star').removeClass('active hover-active'); // Resetea estrellas
46
+ $('#feedback-text').val('');
47
+ feedbackModal.modal('show');
48
+ });
49
+
50
+ // Evento que se dispara DESPUÉS de que el modal se ha ocultado
51
+ $('#feedbackModal').on('hidden.bs.modal', function () {
52
+ $('#feedback-text').val('');
53
+ $('.star').removeClass('active');
54
+ });
55
+
56
+ // Function for the star rating system
57
+ window.gfg = function (rating) {
58
+ $('.star').removeClass('active');
59
+ $('.star').each(function (index) {
60
+ if (index < rating) {
61
+ $(this).addClass('active');
62
+ }
67
63
  });
64
+ };
68
65
 
69
- // Función para el sistema de estrellas
70
- window.gfg = function(rating) {
71
- $('.star').removeClass('active');
72
- $('.star').each(function(index) {
73
- if (index < rating) {
74
- $(this).addClass('active');
66
+ $('.star').hover(
67
+ function () {
68
+ const rating = $(this).data('rating');
69
+ $('.star').removeClass('hover-active');
70
+ $('.star').each(function (index) {
71
+ if ($(this).data('rating') <= rating) {
72
+ $(this).addClass('hover-active');
75
73
  }
76
74
  });
77
- };
75
+ },
76
+ function () {
77
+ $('.star').removeClass('hover-active');
78
+ });
78
79
 
79
- $('.star').hover(
80
- function() {
81
- const rating = $(this).data('rating');
82
- $('.star').removeClass('hover-active');
83
- $('.star').each(function(index) {
84
- if ($(this).data('rating') <= rating) {
85
- $(this).addClass('hover-active');
86
- }
87
- });
88
- },
89
- function() {
90
- $('.star').removeClass('hover-active');
91
- }
92
- );
93
80
  });
94
-
95
- const sendFeedback = async function(message) {
96
- const activeStars = $('.star.active').length;
97
- const data = {
98
- "user_identifier": window.user_identifier,
99
- "message": message,
100
- "rating": activeStars,
101
- };
102
- try {
103
- // Asumiendo que callLLMAPI está definido globalmente en otro archivo (ej. chat_main.js)
104
- const responseData = await callToolkit('/api/feedback', data, "POST");
105
- return responseData;
106
- } catch (error) {
107
- console.error("Error al enviar feedback:", error);
108
- return null;
109
- }
110
- }
@@ -0,0 +1,124 @@
1
+ $(document).ready(function () {
2
+
3
+ let helpContent = null; // Variable para cachear el contenido de ayuda
4
+
5
+ // Evento de clic en el botón de ayuda
6
+ $('#open-help-button').on('click', async function () {
7
+ const helpModal = new bootstrap.Modal(document.getElementById('helpModal'));
8
+ const accordionContainer = $('#help-accordion-container');
9
+ const spinner = $('#help-spinner');
10
+
11
+ // Si el contenido no se ha cargado, hacer la llamada a la API
12
+ if (helpContent) {
13
+ // Si el contenido ya está cacheado, solo muestra el modal
14
+ helpModal.show();
15
+ return;
16
+ }
17
+
18
+ spinner.show();
19
+ accordionContainer.hide();
20
+ helpModal.show();
21
+
22
+ try {
23
+ const helpContent = await callToolkit('/api/help-content', {}, "POST");
24
+
25
+ if (!helpContent) {
26
+ toastr.error('No se pudo cargar la guía de uso. Por favor, intente más tarde.');
27
+ spinner.hide();
28
+ helpModal.hide();
29
+ return;
30
+ }
31
+
32
+ // Construir el HTML del acordeón y mostrarlo
33
+ buildHelpAccordion(helpContent);
34
+ spinner.hide();
35
+ accordionContainer.show();
36
+
37
+ } catch (error) {
38
+ console.error("Error al cargar el contenido de ayuda:", error);
39
+ toastr.error('Ocurrió un error de red al cargar la guía.');
40
+ spinner.hide();
41
+ helpModal.hide();
42
+ }
43
+ });
44
+
45
+ /**
46
+ * Construye dinámicamente el HTML para el acordeón de ayuda a partir de los datos.
47
+ * @param {object} data El objeto JSON con el contenido de ayuda.
48
+ */
49
+ function buildHelpAccordion(data) {
50
+ const container = $('#help-accordion-container');
51
+ container.empty(); // Limpiar cualquier contenido previo
52
+
53
+ let accordionHtml = '';
54
+
55
+ if (data.example_questions) {
56
+ let contentHtml = '';
57
+ data.example_questions.forEach(cat => {
58
+ contentHtml += `<h6 class="fw-bold">${cat.category}</h6><ul>`;
59
+ cat.questions.forEach(q => contentHtml += `<li>${q}</li>`);
60
+ contentHtml += `</ul>`;
61
+ });
62
+ accordionHtml += createAccordionItem('examples', 'Preguntas de Ejemplo', contentHtml, true);
63
+ }
64
+
65
+ if (data.data_sources) {
66
+ let contentHtml = '<dl>';
67
+ data.data_sources.forEach(p => {
68
+ contentHtml += `<dt>${p.source}</dt><dd>${p.description}</dd>`;
69
+ });
70
+ contentHtml += `</dl>`;
71
+ accordionHtml += createAccordionItem('sources', 'Datos disponibles', contentHtml );
72
+ }
73
+
74
+ if (data.best_practices) {
75
+ let contentHtml = '<dl>';
76
+ data.best_practices.forEach(p => {
77
+ contentHtml += `<dt>${p.title}</dt><dd>${p.description}`;
78
+ if (p.example) {
79
+ contentHtml += `<br><small class="text-muted"><em>Ej: "${p.example}"</em></small>`;
80
+ }
81
+ contentHtml += `</dd>`;
82
+ });
83
+ contentHtml += `</dl>`;
84
+ accordionHtml += createAccordionItem('practices', 'Mejores Prácticas', contentHtml);
85
+ }
86
+
87
+ if (data.capabilities) {
88
+ let contentHtml = `<div class="row">`;
89
+ contentHtml += `<div class="col-md-6"><h6 class="fw-bold">Puede hacer:</h6><ul>${data.capabilities.can_do.map(item => `<li>${item}</li>`).join('')}</ul></div>`;
90
+ contentHtml += `<div class="col-md-6"><h6 class="fw-bold">No puede hacer:</h6><ul>${data.capabilities.cannot_do.map(item => `<li>${item}</li>`).join('')}</ul></div>`;
91
+ contentHtml += `</div>`;
92
+ accordionHtml += createAccordionItem('capabilities', 'Capacidades y Límites', contentHtml);
93
+ }
94
+
95
+ container.html(accordionHtml);
96
+ }
97
+
98
+ /**
99
+ * Helper para crear un item del acordeón de Bootstrap.
100
+ * @param {string} id El ID base para los elementos.
101
+ * @param {string} title El título que se muestra en el botón del acordeón.
102
+ * @param {string} contentHtml El HTML que va dentro del cuerpo colapsable.
103
+ * @param {boolean} isOpen Si el item debe estar abierto por defecto.
104
+ * @returns {string} El string HTML del item del acordeón.
105
+ */
106
+ function createAccordionItem(id, title, contentHtml, isOpen = false) {
107
+ const showClass = isOpen ? 'show' : '';
108
+ const collapsedClass = isOpen ? '' : 'collapsed';
109
+
110
+ return `
111
+ <div class="accordion-item">
112
+ <h2 class="accordion-header" id="heading-${id}">
113
+ <button class="accordion-button ${collapsedClass}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${id}" aria-expanded="${isOpen}" aria-controls="collapse-${id}">
114
+ ${title}
115
+ </button>
116
+ </h2>
117
+ <div id="collapse-${id}" class="accordion-collapse collapse ${showClass}" aria-labelledby="heading-${id}" data-bs-parent="#help-accordion-container">
118
+ <div class="accordion-body">
119
+ ${contentHtml}
120
+ </div>
121
+ </div>
122
+ </div>`;
123
+ }
124
+ });