iatoolkit 0.63.1__py3-none-any.whl → 0.67.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 (78) hide show
  1. iatoolkit/__init__.py +2 -0
  2. iatoolkit/base_company.py +1 -20
  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 +41 -5
  8. iatoolkit/locales/en.yaml +167 -0
  9. iatoolkit/locales/es.yaml +163 -0
  10. iatoolkit/repositories/database_manager.py +3 -3
  11. iatoolkit/repositories/document_repo.py +1 -1
  12. iatoolkit/repositories/models.py +2 -3
  13. iatoolkit/repositories/profile_repo.py +0 -4
  14. iatoolkit/services/auth_service.py +14 -9
  15. iatoolkit/services/branding_service.py +32 -22
  16. iatoolkit/services/configuration_service.py +140 -0
  17. iatoolkit/services/dispatcher_service.py +20 -18
  18. iatoolkit/services/document_service.py +5 -2
  19. iatoolkit/services/excel_service.py +15 -11
  20. iatoolkit/services/file_processor_service.py +4 -12
  21. iatoolkit/services/history_service.py +8 -7
  22. iatoolkit/services/i18n_service.py +104 -0
  23. iatoolkit/services/jwt_service.py +7 -9
  24. iatoolkit/services/language_service.py +79 -0
  25. iatoolkit/services/load_documents_service.py +4 -4
  26. iatoolkit/services/mail_service.py +9 -4
  27. iatoolkit/services/onboarding_service.py +10 -4
  28. iatoolkit/services/profile_service.py +58 -38
  29. iatoolkit/services/prompt_manager_service.py +20 -16
  30. iatoolkit/services/query_service.py +15 -14
  31. iatoolkit/services/sql_service.py +6 -2
  32. iatoolkit/services/user_feedback_service.py +16 -14
  33. iatoolkit/static/js/chat_feedback_button.js +57 -87
  34. iatoolkit/static/js/chat_help_content.js +124 -0
  35. iatoolkit/static/js/chat_history_button.js +48 -65
  36. iatoolkit/static/js/chat_main.js +27 -24
  37. iatoolkit/static/js/chat_reload_button.js +28 -45
  38. iatoolkit/static/styles/chat_iatoolkit.css +223 -315
  39. iatoolkit/static/styles/chat_modal.css +63 -97
  40. iatoolkit/static/styles/chat_public.css +107 -0
  41. iatoolkit/static/styles/landing_page.css +0 -1
  42. iatoolkit/templates/_company_header.html +6 -2
  43. iatoolkit/templates/_login_widget.html +42 -0
  44. iatoolkit/templates/base.html +34 -19
  45. iatoolkit/templates/change_password.html +22 -20
  46. iatoolkit/templates/chat.html +58 -27
  47. iatoolkit/templates/chat_modals.html +113 -74
  48. iatoolkit/templates/error.html +12 -13
  49. iatoolkit/templates/forgot_password.html +11 -7
  50. iatoolkit/templates/index.html +8 -3
  51. iatoolkit/templates/login_simulation.html +16 -5
  52. iatoolkit/templates/onboarding_shell.html +0 -1
  53. iatoolkit/templates/signup.html +14 -14
  54. iatoolkit/views/base_login_view.py +12 -1
  55. iatoolkit/views/change_password_view.py +49 -33
  56. iatoolkit/views/forgot_password_view.py +20 -19
  57. iatoolkit/views/help_content_api_view.py +54 -0
  58. iatoolkit/views/history_api_view.py +13 -9
  59. iatoolkit/views/home_view.py +30 -38
  60. iatoolkit/views/init_context_api_view.py +16 -11
  61. iatoolkit/views/llmquery_api_view.py +38 -26
  62. iatoolkit/views/login_simulation_view.py +14 -2
  63. iatoolkit/views/login_view.py +47 -35
  64. iatoolkit/views/logout_api_view.py +26 -22
  65. iatoolkit/views/profile_api_view.py +46 -0
  66. iatoolkit/views/prompt_api_view.py +6 -6
  67. iatoolkit/views/signup_view.py +26 -24
  68. iatoolkit/views/user_feedback_api_view.py +19 -18
  69. iatoolkit/views/verify_user_view.py +30 -29
  70. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/METADATA +40 -22
  71. iatoolkit-0.67.0.dist-info/RECORD +120 -0
  72. iatoolkit-0.67.0.dist-info/licenses/LICENSE +21 -0
  73. iatoolkit/static/styles/chat_info.css +0 -53
  74. iatoolkit/templates/header.html +0 -31
  75. iatoolkit/templates/test.html +0 -9
  76. iatoolkit-0.63.1.dist-info/RECORD +0 -112
  77. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/WHEEL +0 -0
  78. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
@@ -5,21 +5,25 @@
5
5
 
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
-
9
- import logging
8
+ from iatoolkit.services.i18n_service import I18nService
10
9
  from iatoolkit.repositories.profile_repo import ProfileRepo
11
10
  from collections import defaultdict
12
11
  from iatoolkit.repositories.models import Prompt, PromptCategory, Company
13
12
  import os
14
13
  from iatoolkit.common.exceptions import IAToolkitException
15
14
  import importlib.resources
15
+ import logging
16
16
 
17
17
 
18
18
  class PromptService:
19
19
  @inject
20
- def __init__(self, llm_query_repo: LLMQueryRepo, profile_repo: ProfileRepo):
20
+ def __init__(self,
21
+ llm_query_repo: LLMQueryRepo,
22
+ profile_repo: ProfileRepo,
23
+ i18n_service: I18nService):
21
24
  self.llm_query_repo = llm_query_repo
22
25
  self.profile_repo = profile_repo
26
+ self.i18n_service = i18n_service
23
27
 
24
28
  def create_prompt(self,
25
29
  prompt_name: str,
@@ -36,20 +40,20 @@ class PromptService:
36
40
  if is_system_prompt:
37
41
  if not importlib.resources.files('iatoolkit.system_prompts').joinpath(prompt_filename).is_file():
38
42
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
39
- f'No existe el archivo de prompt de sistemas: {prompt_filename}')
43
+ f'missing system prompt file: {prompt_filename}')
40
44
  else:
41
45
  template_dir = f'companies/{company.short_name}/prompts'
42
46
 
43
47
  relative_prompt_path = os.path.join(template_dir, prompt_filename)
44
48
  if not os.path.exists(relative_prompt_path):
45
49
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
46
- f'No existe el archivo de prompt: {relative_prompt_path}')
50
+ f'missing prompt file: {relative_prompt_path}')
47
51
 
48
52
  if custom_fields:
49
53
  for f in custom_fields:
50
54
  if ('data_key' not in f) or ('label' not in f):
51
55
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_PARAMETER,
52
- f'El campo custom_fields debe contener los campos: data_key y label')
56
+ f'The field "custom_fields" must contain the following keys: data_key y label')
53
57
 
54
58
  # add default value for data_type
55
59
  if 'type' not in f:
@@ -82,20 +86,20 @@ class PromptService:
82
86
  user_prompt = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
83
87
  if not user_prompt:
84
88
  raise IAToolkitException(IAToolkitException.ErrorType.DOCUMENT_NOT_FOUND,
85
- f"No se encontró el prompt '{prompt_name}' para la empresa '{company.short_name}'")
89
+ f"prompt not found '{prompt_name}' for company '{company.short_name}'")
86
90
 
87
91
  prompt_file = f'companies/{company.short_name}/prompts/{user_prompt.filename}'
88
92
  absolute_filepath = os.path.join(execution_dir, prompt_file)
89
93
  if not os.path.exists(absolute_filepath):
90
94
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
91
- f"El archivo para el prompt '{prompt_name}' no existe: {absolute_filepath}")
95
+ f"prompt file '{prompt_name}' does not exist: {absolute_filepath}")
92
96
 
93
97
  try:
94
98
  with open(absolute_filepath, 'r', encoding='utf-8') as f:
95
99
  user_prompt_content = f.read()
96
100
  except Exception as e:
97
101
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
98
- f"Error leyendo el archivo de prompt '{prompt_name}' en {absolute_filepath}: {e}")
102
+ f"error while reading prompt: '{prompt_name}' in this pathname {absolute_filepath}: {e}")
99
103
 
100
104
  return user_prompt_content
101
105
 
@@ -105,9 +109,9 @@ class PromptService:
105
109
  raise
106
110
  except Exception as e:
107
111
  logging.exception(
108
- f"Error al obtener el contenido del prompt para la empresa '{company.short_name}' y prompt '{prompt_name}': {e}")
112
+ f"error loading prompt '{prompt_name}' content for '{company.short_name}': {e}")
109
113
  raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
110
- f'Error al obtener el contenido del prompt "{prompt_name}" para la empresa {company.short_name}: {str(e)}')
114
+ f'error loading prompt "{prompt_name}" content for company {company.short_name}: {str(e)}')
111
115
 
112
116
  def get_system_prompt(self):
113
117
  try:
@@ -121,10 +125,10 @@ class PromptService:
121
125
  content = importlib.resources.read_text('iatoolkit.system_prompts', prompt.filename)
122
126
  system_prompt_content.append(content)
123
127
  except FileNotFoundError:
124
- logging.warning(f"El archivo para el prompt de sistema no existe en el paquete: {prompt.filename}")
128
+ logging.warning(f"Prompt file does not exist in the package: {prompt.filename}")
125
129
  except Exception as e:
126
130
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
127
- f"Error leyendo el archivo de prompt del sistema '{prompt.filename}': {e}")
131
+ f"error reading system prompt '{prompt.filename}': {e}")
128
132
 
129
133
  # join the system prompts into a single string
130
134
  return "\n".join(system_prompt_content)
@@ -135,14 +139,14 @@ class PromptService:
135
139
  logging.exception(
136
140
  f"Error al obtener el contenido del prompt de sistema: {e}")
137
141
  raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
138
- f'Error al obtener el contenido de los prompts de sistema": {str(e)}')
142
+ f'error reading the system prompts": {str(e)}')
139
143
 
140
144
  def get_user_prompts(self, company_short_name: str) -> dict:
141
145
  try:
142
146
  # validate company
143
147
  company = self.profile_repo.get_company_by_short_name(company_short_name)
144
148
  if not company:
145
- return {'error': f'No existe la empresa: {company_short_name}'}
149
+ return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
146
150
 
147
151
  # get all the prompts
148
152
  all_prompts = self.llm_query_repo.get_prompts(company)
@@ -183,6 +187,6 @@ class PromptService:
183
187
  return {'message': categorized_prompts}
184
188
 
185
189
  except Exception as e:
186
- logging.error(f"Error en get_prompts: {e}")
190
+ logging.error(f"error in get_prompts: {e}")
187
191
  return {'error': str(e)}
188
192
 
@@ -8,6 +8,7 @@ from iatoolkit.services.profile_service import ProfileService
8
8
  from iatoolkit.repositories.document_repo import DocumentRepo
9
9
  from iatoolkit.repositories.profile_repo import ProfileRepo
10
10
  from iatoolkit.services.document_service import DocumentService
11
+ from iatoolkit.services.i18n_service import I18nService
11
12
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
12
13
  from iatoolkit.repositories.models import Task
13
14
  from iatoolkit.services.dispatcher_service import Dispatcher
@@ -37,6 +38,7 @@ class QueryService:
37
38
  llmquery_repo: LLMQueryRepo,
38
39
  profile_repo: ProfileRepo,
39
40
  prompt_service: PromptService,
41
+ i18n_service: I18nService,
40
42
  util: Utility,
41
43
  dispatcher: Dispatcher,
42
44
  session_context: UserSessionContextService
@@ -47,6 +49,7 @@ class QueryService:
47
49
  self.llmquery_repo = llmquery_repo
48
50
  self.profile_repo = profile_repo
49
51
  self.prompt_service = prompt_service
52
+ self.i18n_service = i18n_service
50
53
  self.util = util
51
54
  self.dispatcher = dispatcher
52
55
  self.session_context = session_context
@@ -56,7 +59,7 @@ class QueryService:
56
59
  self.model = os.getenv("LLM_MODEL", "")
57
60
  if not self.model:
58
61
  raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
59
- "La variable de entorno 'LLM_MODEL' no está configurada.")
62
+ "missing ENV variable 'LLM_MODEL' configuration.")
60
63
 
61
64
  def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
62
65
  # this method read the user/company context from the database and renders the system prompt
@@ -127,7 +130,7 @@ class QueryService:
127
130
  lock_key = f"lock:context:{company_short_name}/{user_identifier}"
128
131
  if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
129
132
  logging.warning(
130
- f"Intento de reconstruir contexto para {user_identifier} mientras ya estaba en progreso. Se omite.")
133
+ f"try to rebuild context for user {user_identifier} while is still in process, ignored.")
131
134
  return
132
135
 
133
136
  try:
@@ -138,11 +141,9 @@ class QueryService:
138
141
  prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name,
139
142
  user_identifier)
140
143
  if not prepared_context:
141
- logging.info(
142
- f"No se requiere reconstrucción de contexto para {company_short_name}/{user_identifier}. Finalización rápida.")
143
144
  return
144
145
 
145
- logging.info(f"Enviando contexto al LLM para {company_short_name}/{user_identifier}...")
146
+ logging.info(f"sending context to LLM for: {company_short_name}/{user_identifier}...")
146
147
 
147
148
  # Limpiar solo el historial de chat y el ID de respuesta anterior
148
149
  self.session_context.clear_llm_history(company_short_name, user_identifier)
@@ -161,9 +162,9 @@ class QueryService:
161
162
  self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
162
163
 
163
164
  logging.info(
164
- f"Contexto de {company_short_name}/{user_identifier} establecido en {int(time.time() - start_time)} seg.")
165
+ f"Context for: {company_short_name}/{user_identifier} settled in {int(time.time() - start_time)} sec.")
165
166
  except Exception as e:
166
- logging.exception(f"Error en finalize_context_rebuild para {company_short_name}: {e}")
167
+ logging.exception(f"Error in finalize_context_rebuild for {company_short_name}: {e}")
167
168
  raise e
168
169
  finally:
169
170
  # --- Liberar el Bloqueo ---
@@ -181,11 +182,11 @@ class QueryService:
181
182
  company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
182
183
  if not company:
183
184
  return {"error": True,
184
- "error_message": f'No existe Company ID: {company_short_name}'}
185
+ "error_message": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
185
186
 
186
187
  if not prompt_name and not question:
187
188
  return {"error": True,
188
- "error_message": f'Hola, cual es tu pregunta?'}
189
+ "error_message": self.i18n_service.t('services.start_query')}
189
190
 
190
191
  # get the previous response_id and context history
191
192
  previous_response_id = None
@@ -196,7 +197,7 @@ class QueryService:
196
197
  previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
197
198
  if not previous_response_id:
198
199
  return {'error': True,
199
- "error_message": f"No se encontró 'previous_response_id' para '{company.short_name}/{user_identifier}'. Reinicia el contexto para continuar."
200
+ "error_message": self.i18n_service.t('errors.services.missing_response_id', company_short_name=company.short_name, user_identifier=user_identifier)
200
201
  }
201
202
  elif self.util.is_gemini_model(self.model):
202
203
  # check the length of the context_history and remove old messages
@@ -294,7 +295,7 @@ class QueryService:
294
295
  return len(history) >= 1
295
296
  return False
296
297
  except Exception as e:
297
- logging.warning(f"Error verificando caché de contexto: {e}")
298
+ logging.warning(f"error verifying context cache: {e}")
298
299
  return False
299
300
 
300
301
  def load_files_for_context(self, files: list) -> str:
@@ -353,7 +354,7 @@ class QueryService:
353
354
  try:
354
355
  total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
355
356
  except Exception as e:
356
- logging.error(f"Error al calcular tokens del historial: {e}. No se pudo recortar el contexto.")
357
+ logging.error(f"error counting tokens for history: {e}.")
357
358
  return
358
359
 
359
360
  # Si se excede el límite, eliminar mensajes antiguos (empezando por el segundo)
@@ -364,8 +365,8 @@ class QueryService:
364
365
  removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
365
366
  total_tokens -= removed_tokens
366
367
  logging.warning(
367
- f"Historial de contexto ({total_tokens + removed_tokens} tokens) excedía el límite de {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
368
- f"Nuevo total: {total_tokens} tokens."
368
+ f"history tokens ({total_tokens + removed_tokens} tokens) exceed the limit of: {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
369
+ f"new context: {total_tokens} tokens."
369
370
  )
370
371
  except IndexError:
371
372
  # Se produce si solo queda el mensaje del sistema, el bucle debería detenerse.
@@ -6,6 +6,7 @@
6
6
  from iatoolkit.repositories.database_manager import DatabaseManager
7
7
 
8
8
  from iatoolkit.common.util import Utility
9
+ from iatoolkit.services.i18n_service import I18nService
9
10
  from sqlalchemy import text
10
11
  from injector import inject
11
12
  import json
@@ -14,8 +15,11 @@ from iatoolkit.common.exceptions import IAToolkitException
14
15
 
15
16
  class SqlService:
16
17
  @inject
17
- def __init__(self,util: Utility):
18
+ def __init__(self,
19
+ util: Utility,
20
+ i18n_service: I18nService):
18
21
  self.util = util
22
+ self.i18n_service = i18n_service
19
23
 
20
24
  def exec_sql(self, db_manager: DatabaseManager, sql_statement: str) -> str:
21
25
  """
@@ -54,7 +58,7 @@ class SqlService:
54
58
 
55
59
  error_message = str(e)
56
60
  if 'timed out' in str(e):
57
- error_message = 'Intentalo de nuevo, se agoto el tiempo de espera'
61
+ error_message = self.i18n_service.t('errors.timeout')
58
62
 
59
63
  raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
60
64
  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
- }