iatoolkit 0.59.1__py3-none-any.whl → 0.60.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 (32) hide show
  1. iatoolkit/base_company.py +3 -1
  2. iatoolkit/common/routes.py +12 -21
  3. iatoolkit/iatoolkit.py +5 -5
  4. iatoolkit/repositories/models.py +1 -1
  5. iatoolkit/services/auth_service.py +32 -25
  6. iatoolkit/services/profile_service.py +1 -0
  7. iatoolkit/services/user_feedback_service.py +65 -26
  8. iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +5 -10
  9. iatoolkit/static/js/chat_main.js +0 -8
  10. iatoolkit/static/js/{chat_onboarding.js → chat_onboarding_button.js} +0 -1
  11. iatoolkit/static/js/{chat_context_reload.js → chat_reload_button.js} +2 -4
  12. iatoolkit/static/styles/chat_iatoolkit.css +44 -0
  13. iatoolkit/templates/chat.html +18 -10
  14. iatoolkit/templates/onboarding_shell.html +1 -1
  15. iatoolkit/views/change_password_view.py +2 -2
  16. iatoolkit/views/external_login_view.py +5 -11
  17. iatoolkit/views/file_store_api_view.py +7 -9
  18. iatoolkit/views/history_api_view.py +8 -8
  19. iatoolkit/views/init_context_api_view.py +2 -4
  20. iatoolkit/views/llmquery_api_view.py +1 -3
  21. iatoolkit/views/logout_api_view.py +45 -0
  22. iatoolkit/views/prompt_api_view.py +5 -5
  23. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  24. iatoolkit/views/tasks_review_api_view.py +55 -0
  25. iatoolkit/views/user_feedback_api_view.py +6 -18
  26. {iatoolkit-0.59.1.dist-info → iatoolkit-0.60.0.dist-info}/METADATA +1 -1
  27. {iatoolkit-0.59.1.dist-info → iatoolkit-0.60.0.dist-info}/RECORD +30 -30
  28. iatoolkit/views/chat_token_request_view.py +0 -98
  29. iatoolkit/views/tasks_review_view.py +0 -83
  30. /iatoolkit/static/js/{chat_history.js → chat_history_button.js} +0 -0
  31. {iatoolkit-0.59.1.dist-info → iatoolkit-0.60.0.dist-info}/WHEEL +0 -0
  32. {iatoolkit-0.59.1.dist-info → iatoolkit-0.60.0.dist-info}/top_level.txt +0 -0
iatoolkit/base_company.py CHANGED
@@ -29,11 +29,13 @@ class BaseCompany(ABC):
29
29
  def _create_company(self,
30
30
  short_name: str,
31
31
  name: str,
32
+ parameters: dict | None = None,
32
33
  branding: dict | None = None,
33
- onboarding_cards: dict | None = None
34
+ onboarding_cards: dict | None = None,
34
35
  ) -> Company:
35
36
  company_obj = Company(short_name=short_name,
36
37
  name=name,
38
+ parameters=parameters,
37
39
  branding=branding,
38
40
  onboarding_cards=onboarding_cards)
39
41
  self.company = self.profile_repo.create_company(company_obj)
@@ -3,17 +3,9 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from flask import render_template, redirect, flash, url_for,send_from_directory, current_app, abort
7
- from iatoolkit.common.session_manager import SessionManager
6
+ from flask import render_template, redirect, url_for,send_from_directory, current_app, abort
8
7
  from flask import jsonify
9
8
  from iatoolkit.views.history_api_view import HistoryApiView
10
- import os
11
-
12
-
13
- def logout(company_short_name: str):
14
- SessionManager.clear()
15
- flash("Has cerrado sesión correctamente", "info")
16
- return redirect(url_for('index', company_short_name=company_short_name))
17
9
 
18
10
 
19
11
  # this function register all the views
@@ -22,8 +14,8 @@ def register_views(injector, app):
22
14
  from iatoolkit.views.index_view import IndexView
23
15
  from iatoolkit.views.init_context_api_view import InitContextApiView
24
16
  from iatoolkit.views.llmquery_api_view import LLMQueryApiView
25
- from iatoolkit.views.tasks_view import TaskView
26
- from iatoolkit.views.tasks_review_view import TaskReviewView
17
+ from iatoolkit.views.tasks_api_view import TaskApiView
18
+ from iatoolkit.views.tasks_review_api_view import TaskReviewApiView
27
19
  from iatoolkit.views.login_simulation_view import LoginSimulationView
28
20
  from iatoolkit.views.signup_view import SignupView
29
21
  from iatoolkit.views.verify_user_view import VerifyAccountView
@@ -32,9 +24,10 @@ def register_views(injector, app):
32
24
  from iatoolkit.views.file_store_api_view import FileStoreApiView
33
25
  from iatoolkit.views.user_feedback_api_view import UserFeedbackApiView
34
26
  from iatoolkit.views.prompt_api_view import PromptApiView
35
- from iatoolkit.views.chat_token_request_view import ChatTokenRequestView
36
27
  from iatoolkit.views.login_view import LoginView, FinalizeContextView
37
28
  from iatoolkit.views.external_login_view import ExternalLoginView, RedeemTokenApiView
29
+ from iatoolkit.views.logout_api_view import LogoutApiView
30
+
38
31
 
39
32
  # iatoolkit home page
40
33
  app.add_url_rule('/<company_short_name>', view_func=IndexView.as_view('index'))
@@ -57,23 +50,21 @@ def register_views(injector, app):
57
50
  view_func=FinalizeContextView.as_view('finalize_with_token')
58
51
  )
59
52
 
53
+ # logout
54
+ app.add_url_rule('/<company_short_name>/api/logout',
55
+ view_func=LogoutApiView.as_view('logout'))
56
+
60
57
  # this endpoint is called by the JS for changing the token for a session
61
58
  app.add_url_rule('/<string:company_short_name>/api/redeem_token',
62
59
  view_func = RedeemTokenApiView.as_view('redeem_token'))
63
60
 
64
- # this endpoint is for requesting a chat token for external users
65
- app.add_url_rule('/auth/chat_token',
66
- view_func=ChatTokenRequestView.as_view('chat-token'))
67
-
68
- # init (reset) the company context (with api-key)
61
+ # init (reset) the company context
69
62
  app.add_url_rule('/<company_short_name>/api/init-context',
70
63
  view_func=InitContextApiView.as_view('init-context'),
71
64
  methods=['POST', 'OPTIONS'])
72
65
 
73
66
  # register new user, account verification and forgot password
74
67
  app.add_url_rule('/<company_short_name>/signup',view_func=SignupView.as_view('signup'))
75
- app.add_url_rule('/<company_short_name>/logout', 'logout', logout)
76
- app.add_url_rule('/logout', 'logout', logout)
77
68
  app.add_url_rule('/<company_short_name>/verify/<token>', view_func=VerifyAccountView.as_view('verify_account'))
78
69
  app.add_url_rule('/<company_short_name>/forgot-password', view_func=ForgotPasswordView.as_view('forgot_password'))
79
70
  app.add_url_rule('/<company_short_name>/change-password/<token>', view_func=ChangePasswordView.as_view('change_password'))
@@ -90,8 +81,8 @@ def register_views(injector, app):
90
81
  app.add_url_rule('/<company_short_name>/api/history', view_func=HistoryApiView.as_view('history'))
91
82
 
92
83
  # tasks management endpoints: create task, and review answer
93
- app.add_url_rule('/tasks', view_func=TaskView.as_view('tasks'))
94
- app.add_url_rule('/tasks/review/<int:task_id>', view_func=TaskReviewView.as_view('tasks-review'))
84
+ app.add_url_rule('/tasks', view_func=TaskApiView.as_view('tasks'))
85
+ app.add_url_rule('/tasks/review/<int:task_id>', view_func=TaskReviewApiView.as_view('tasks-review'))
95
86
 
96
87
  # this endpoint is for upload documents into the vector store (api-key)
97
88
  app.add_url_rule('/api/load', view_func=FileStoreApiView.as_view('load_api'))
iatoolkit/iatoolkit.py CHANGED
@@ -16,10 +16,10 @@ import os
16
16
  from typing import Optional, Dict, Any
17
17
  from iatoolkit.repositories.database_manager import DatabaseManager
18
18
  from werkzeug.middleware.proxy_fix import ProxyFix
19
- from injector import Binder, singleton, Injector
19
+ from injector import Binder, Injector, singleton
20
20
  from importlib.metadata import version as _pkg_version, PackageNotFoundError
21
21
 
22
- IATOOLKIT_VERSION = "0.59.1"
22
+ IATOOLKIT_VERSION = "0.60.0"
23
23
 
24
24
  # global variable for the unique instance of IAToolkit
25
25
  _iatoolkit_instance: Optional['IAToolkit'] = None
@@ -52,7 +52,7 @@ class IAToolkit:
52
52
  self.app = None
53
53
  self.db_manager = None
54
54
  self._injector = None
55
- self.version = IATOOLKIT_VERSION
55
+ self.version = IATOOLKIT_VERSION # default version
56
56
 
57
57
  @classmethod
58
58
  def get_instance(cls) -> 'IAToolkit':
@@ -324,8 +324,8 @@ class IAToolkit:
324
324
  from iatoolkit.services.auth_service import AuthService
325
325
  from iatoolkit.common.util import Utility
326
326
 
327
- binder.bind(LLMProxy, to=LLMProxy, scope=singleton)
328
- binder.bind(llmClient, to=llmClient, scope=singleton)
327
+ binder.bind(LLMProxy, to=LLMProxy)
328
+ binder.bind(llmClient, to=llmClient)
329
329
  binder.bind(GoogleChatApp, to=GoogleChatApp)
330
330
  binder.bind(MailApp, to=MailApp)
331
331
  binder.bind(AuthService, to=AuthService)
@@ -60,7 +60,7 @@ class Company(Base):
60
60
 
61
61
  branding = Column(JSON, nullable=True)
62
62
  onboarding_cards = Column(JSON, nullable=True)
63
- parameters = Column(JSON, nullable=True, default={})
63
+ parameters = Column(JSON, nullable=True)
64
64
  created_at = Column(DateTime, default=datetime.now)
65
65
  allow_jwt = Column(Boolean, default=True, nullable=True)
66
66
 
@@ -91,9 +91,10 @@ class AuthService:
91
91
  )
92
92
  return {'success': False, 'error': 'No se pudo crear la sesión del usuario'}
93
93
 
94
- def verify(self) -> dict:
94
+ def verify(self, anonymous: bool = False) -> dict:
95
95
  """
96
96
  Verifies the current request and identifies the user.
97
+ If anonymous is True the non-presence of use_identifier is ignored
97
98
 
98
99
  Returns a dictionary with:
99
100
  - success: bool
@@ -118,31 +119,37 @@ class AuthService:
118
119
  if isinstance(auth, str) and auth.lower().startswith('bearer '):
119
120
  api_key = auth.split(' ', 1)[1].strip()
120
121
 
121
- if api_key:
122
- api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
123
- if not api_key_entry:
124
- logging.info(f"Invalid or inactive API Key {api_key}")
125
- return {"success": False, "error_message": "Invalid or inactive API Key", "status_code": 401}
122
+ if not api_key:
123
+ # --- Failure: No valid credentials found ---
124
+ logging.info(f"Authentication required. No session cookie or API Key provided.")
125
+ return {"success": False,
126
+ "error_message": "Authentication required. No session cookie or API Key provided.",
127
+ "status_code": 401}
128
+
129
+ # check if the api-key is valid and active
130
+ api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
131
+ if not api_key_entry:
132
+ logging.info(f"Invalid or inactive API Key {api_key}")
133
+ return {"success": False, "error_message": "Invalid or inactive API Key",
134
+ "status_code": 402}
135
+
136
+ # get the company from the api_key_entry
137
+ company = api_key_entry.company
138
+
139
+ # For API calls, the external_user_id must be provided in the request.
140
+ data = request.get_json(silent=True) or {}
141
+ user_identifier = data.get('user_identifier', '')
142
+ if not anonymous and not user_identifier:
143
+ logging.info(f"No user_identifier provided for API call.")
144
+ return {"success": False, "error_message": "No user_identifier provided for API call.",
145
+ "status_code": 403}
146
+
147
+ return {
148
+ "success": True,
149
+ "company_short_name": company.short_name,
150
+ "user_identifier": user_identifier
151
+ }
126
152
 
127
- # obtain the company from the api_key_entry
128
- company = api_key_entry.company
129
-
130
- # For API calls, the external_user_id must be provided in the request.
131
- user_identifier = ''
132
- if request.is_json:
133
- data = request.get_json() or {}
134
- user_identifier = data.get('user_identifier', '')
135
-
136
- return {
137
- "success": True,
138
- "company_short_name": company.short_name,
139
- "user_identifier": user_identifier
140
- }
141
-
142
- # --- Failure: No valid credentials found ---
143
- logging.info(f"Authentication required. No session cookie or API Key provided. session: {str(session_info)}")
144
- return {"success": False, "error_message": "Authentication required. No session cookie or API Key provided.",
145
- "status_code": 402}
146
153
 
147
154
  def log_access(self,
148
155
  company_short_name: str,
@@ -131,6 +131,7 @@ class ProfileService:
131
131
  "profile": profile
132
132
  }
133
133
 
134
+
134
135
  def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
135
136
  """
136
137
  Fetches a user profile directly by their identifier, bypassing the Flask session.
@@ -3,62 +3,101 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from iatoolkit.repositories.models import UserFeedback
6
+ from iatoolkit.repositories.models import UserFeedback, Company
7
7
  from injector import inject
8
8
  from iatoolkit.repositories.profile_repo import ProfileRepo
9
9
  from iatoolkit.infra.google_chat_app import GoogleChatApp
10
+ from iatoolkit.infra.mail_app import MailApp # <-- 1. Importar MailApp
10
11
  import logging
11
12
 
12
13
 
13
14
  class UserFeedbackService:
14
15
  @inject
15
- def __init__(self, profile_repo: ProfileRepo, google_chat_app: GoogleChatApp):
16
+ def __init__(self,
17
+ profile_repo: ProfileRepo,
18
+ google_chat_app: GoogleChatApp,
19
+ mail_app: MailApp):
16
20
  self.profile_repo = profile_repo
17
21
  self.google_chat_app = google_chat_app
22
+ self.mail_app = mail_app
23
+
24
+ def _send_google_chat_notification(self, space_name: str, message_text: str):
25
+ """Envía una notificación de feedback a un espacio de Google Chat."""
26
+ try:
27
+ chat_data = {
28
+ "type": "MESSAGE_TRIGGER",
29
+ "space": {"name": space_name},
30
+ "message": {"text": message_text}
31
+ }
32
+ chat_result = self.google_chat_app.send_message(message_data=chat_data)
33
+ if not chat_result.get('success'):
34
+ logging.warning(f"Error al enviar notificación a Google Chat: {chat_result.get('message')}")
35
+ except Exception as e:
36
+ logging.exception(f"Fallo inesperado al enviar notificación a Google Chat: {e}")
37
+
38
+ def _send_email_notification(self, destination_email: str, company_name: str, message_text: str):
39
+ """Envía una notificación de feedback por correo electrónico."""
40
+ try:
41
+ subject = f"Nuevo Feedback de {company_name}"
42
+ # Convertir el texto plano a un HTML simple para mantener los saltos de línea
43
+ html_body = message_text.replace('\n', '<br>')
44
+ self.mail_app.send_email(to=destination_email, subject=subject, body=html_body)
45
+ except Exception as e:
46
+ logging.exception(f"Fallo inesperado al enviar email de feedback: {e}")
47
+
48
+ def _handle_notification(self, company: Company, message_text: str):
49
+ """Lee la configuración de la empresa y envía la notificación al canal correspondiente."""
50
+ feedback_params = company.parameters.get('user_feedback')
51
+ if not isinstance(feedback_params, dict):
52
+ logging.warning(f"No se encontró configuración de 'user_feedback' para la empresa {company.short_name}.")
53
+ return
54
+
55
+ # get channel and destination
56
+ channel = feedback_params.get('channel')
57
+ destination = feedback_params.get('destination')
58
+ if not channel or not destination:
59
+ logging.warning(f"Configuración 'user_feedback' incompleta para {company.short_name}. Faltan 'channel' o 'destination'.")
60
+ return
61
+
62
+ if channel == 'google_chat':
63
+ self._send_google_chat_notification(space_name=destination, message_text=message_text)
64
+ elif channel == 'email':
65
+ self._send_email_notification(destination_email=destination, company_name=company.short_name, message_text=message_text)
66
+ else:
67
+ logging.warning(f"Canal de feedback '{channel}' no reconocido para la empresa {company.short_name}.")
18
68
 
19
69
  def new_feedback(self,
20
70
  company_short_name: str,
21
71
  message: str,
22
72
  user_identifier: str,
23
- space: str = None,
24
- type: str = None,
25
73
  rating: int = None) -> dict:
26
74
  try:
27
- # validate company
75
+ # 1. Validar empresa
28
76
  company = self.profile_repo.get_company_by_short_name(company_short_name)
29
77
  if not company:
30
78
  return {'error': f'No existe la empresa: {company_short_name}'}
31
79
 
32
- # send notification to Google Chat
33
- chat_message = f"*Nuevo feedback de {company_short_name}*:\n*Usuario:* {user_identifier}\n*Mensaje:* {message}\n*Calificación:* {rating}"
34
-
35
- # TO DO: get the space and type from the input data
36
- chat_data = {
37
- "type": type,
38
- "space": {
39
- "name": space
40
- },
41
- "message": {
42
- "text": chat_message
43
- }
44
- }
45
-
46
- chat_result = self.google_chat_app.send_message(message_data=chat_data)
47
- if not chat_result.get('success'):
48
- logging.warning(f"Error al enviar notificación a Google Chat: {chat_result.get('message')}")
80
+ # 2. Enviar notificación según la configuración de la empresa
81
+ notification_text = (f"*Nuevo feedback de {company_short_name}*:\n"
82
+ f"*Usuario:* {user_identifier}\n"
83
+ f"*Mensaje:* {message}\n"
84
+ f"*Calificación:* {rating if rating is not None else 'N/A'}")
85
+ self._handle_notification(company, notification_text)
49
86
 
50
- # create the UserFeedback object
51
- new_feedback = UserFeedback(
87
+ # 3. Guardar el feedback en la base de datos (independientemente del éxito de la notificación)
88
+ new_feedback_obj = UserFeedback(
52
89
  company_id=company.id,
53
90
  message=message,
54
91
  user_identifier=user_identifier,
55
92
  rating=rating
56
93
  )
57
- new_feedback = self.profile_repo.save_feedback(new_feedback)
58
- if not new_feedback:
94
+ saved_feedback = self.profile_repo.save_feedback(new_feedback_obj)
95
+ 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}")
59
97
  return {'error': 'No se pudo guardar el feedback'}
60
98
 
61
99
  return {'message': 'Feedback guardado correctamente'}
62
100
 
63
101
  except Exception as e:
102
+ logging.exception(f"Error crítico en el servicio de feedback: {e}")
64
103
  return {'error': str(e)}
@@ -42,14 +42,11 @@ $(document).ready(function () {
42
42
  submitButton.html('<i class="bi bi-send me-1 icon-spaced"></i>Enviando...');
43
43
 
44
44
  const response = await sendFeedback(feedbackText);
45
-
46
45
  $('#feedbackModal').modal('hide');
47
-
48
- if (response) {
49
- Swal.fire({ icon: 'success', title: 'Feedback enviado', text: 'Gracias por tu comentario.' });
50
- } else {
51
- Swal.fire({ icon: 'error', title: 'Error', text: 'No se pudo enviar el feedback, intente nuevamente.' });
52
- }
46
+ if (response)
47
+ toastr.success('¡Gracias por tu comentario!', 'Feedback Enviado');
48
+ else
49
+ toastr.error('No se pudo enviar el feedback, por favor intente nuevamente.', 'Error');
53
50
  });
54
51
 
55
52
  // Evento para abrir el modal de feedback
@@ -101,12 +98,10 @@ const sendFeedback = async function(message) {
101
98
  "user_identifier": window.user_identifier,
102
99
  "message": message,
103
100
  "rating": activeStars,
104
- "space": "spaces/AAQAupQldd4", // Este valor podría necesitar ser dinámico
105
- "type": "MESSAGE_TRIGGER"
106
101
  };
107
102
  try {
108
103
  // Asumiendo que callLLMAPI está definido globalmente en otro archivo (ej. chat_main.js)
109
- const responseData = await callToolkit('/feedback', data, "POST");
104
+ const responseData = await callToolkit('/api/feedback', data, "POST");
110
105
  return responseData;
111
106
  } catch (error) {
112
107
  console.error("Error al enviar feedback:", error);
@@ -1,7 +1,6 @@
1
1
  // Global variables for request management
2
2
  let isRequestInProgress = false;
3
3
  let abortController = null;
4
-
5
4
  let selectedPrompt = null; // Will hold a lightweight prompt object
6
5
 
7
6
  $(document).ready(function () {
@@ -325,13 +324,6 @@ const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
325
324
  clearTimeout(timeoutId);
326
325
 
327
326
  if (!response.ok) {
328
- if (response.status === 401) {
329
- const errorMessage = `Tu sesión ha expirado. `;
330
- const errorIcon = '<i class="bi bi-exclamation-triangle"></i>';
331
- const infrastructureError = $('<div>').addClass('error-section').html(errorIcon + `<p>${errorMessage}</p>`);
332
- displayBotMessage(infrastructureError);
333
- return null;
334
- }
335
327
  try {
336
328
  // Intentamos leer el error como JSON, que es el formato esperado de nuestra API.
337
329
  const errorData = await response.json();
@@ -1,4 +1,3 @@
1
- // static/js/chat_onboarding.js
2
1
  (function (global) {
3
2
  function qs(root, sel) { return (typeof sel === 'string') ? root.querySelector(sel) : sel; }
4
3
 
@@ -30,12 +30,10 @@ document.addEventListener('DOMContentLoaded', function() {
30
30
  // 4. Procesar la respuesta
31
31
  // callToolkit devuelve null si hubo un error que ya mostró en el chat.
32
32
  if (data) {
33
- if (data.status === 'OK') {
33
+ if (data.status === 'OK')
34
34
  toastr.success(data.message || 'Contexto recargado exitosamente.');
35
- } else {
36
- // El servidor respondió 200 OK pero con un mensaje de error en el cuerpo
35
+ else
37
36
  toastr.error(data.error_message || 'Ocurrió un error desconocido durante la recarga.');
38
- }
39
37
  } else {
40
38
  // Si data es null, callToolkit ya manejó el error (mostrando un mensaje en el chat).
41
39
  // Añadimos un toast para notificar al usuario que algo falló.
@@ -455,3 +455,47 @@
455
455
  #send-button i {
456
456
  color: var(--brand-send-button-color);
457
457
  }
458
+
459
+ /* Estilos personalizados para Toastr usando las variables de Branding */
460
+
461
+ /* --- Toast de Información (Usa el color primario de la marca) --- */
462
+ .toast-info {
463
+ /* ¡Importante! Usamos las variables CSS de tu BrandingService */
464
+ background-color: var(--brand-primary-color) !important;
465
+ color: var(--brand-text-on-primary) !important;
466
+ opacity: 0.95 !important;
467
+ }
468
+ .toast-info .toast-progress {
469
+ /* Usamos un color ligeramente más oscuro o una variante,
470
+ pero para simplificar, podemos empezar con el mismo */
471
+ background-color: rgba(0, 0, 0, 0.2) !important;
472
+ }
473
+
474
+ /* --- Toast de Éxito (Usa el verde por defecto o uno de marca si lo defines) --- */
475
+ .toast-success {
476
+ background-color: #198754 !important; /* Puedes crear una variable --brand-success-color si quieres */
477
+ color: #ffffff !important;
478
+ opacity: 0.95 !important;
479
+ }
480
+ .toast-success .toast-progress {
481
+ background-color: rgba(0, 0, 0, 0.2) !important;
482
+ }
483
+
484
+ /* --- Toast de Error (Usa el color de peligro de la marca) --- */
485
+ .toast-error {
486
+ background-color: var(--brand-danger-color) !important;
487
+ color: var(--brand-text-on-primary) !important; /* Asumimos texto blanco sobre el color de peligro */
488
+ opacity: 0.95 !important;
489
+ }
490
+ .toast-error .toast-progress {
491
+ background-color: rgba(0, 0, 0, 0.2) !important;
492
+ }
493
+
494
+ /* Opcional: Estilo para el botón de cierre para que contraste bien */
495
+ .toast-close-button {
496
+ color: var(--brand-text-on-primary) !important;
497
+ text-shadow: none !important;
498
+ }
499
+ .toast-close-button:hover {
500
+ opacity: 0.8;
501
+ }
@@ -40,7 +40,8 @@
40
40
  <div class="vr mx-3"></div>
41
41
 
42
42
  <!-- 3. Iconos de Acción -->
43
- <a href="javascript:void(0);" id="history-button"
43
+ <a href="javascript:void(0);"
44
+ id="history-button"
44
45
  class="action-icon-style" title="Historial con mis consultas" style="color: {{ branding.header_text_color }};">
45
46
  <i class="bi bi-clock-history"></i>
46
47
  </a>
@@ -51,19 +52,23 @@
51
52
  style="color: {{ branding.header_text_color }};">
52
53
  <i class="bi bi-arrow-clockwise"></i>
53
54
  </a>
54
- <a href="javascript:void(0);" id="send-feedback-button"
55
+ <a href="javascript:void(0);"
56
+ id="send-feedback-button"
55
57
  class="ms-3 action-icon-style" title="Tu feedback es muy importante" style="color: {{ branding.header_text_color }};">
56
58
  <i class="bi bi-emoji-smile"></i>
57
59
  </a>
58
- <a href="javascript:void(0);" id="onboarding-button"
60
+ <a href="javascript:void(0);"
61
+ id="onboarding-button"
59
62
  class="ms-3 action-icon-style" title="Ver onboarding"
60
63
  style="color: {{ branding.header_text_color }};">
61
64
  <i class="bi bi-lightbulb"></i>
62
65
  </a>
63
66
 
64
67
  <!-- Icono de cerrar sesión (al final) -->
65
- <a href="{{ url_for('logout', company_short_name=company_short_name, _external=True) }}"
66
- class="ms-3 action-icon-style" title="Cerrar sesión" style="color: {{ branding.header_text_color }} !important;">
68
+ <a href="javascript:void(0);"
69
+ id="logout-button"
70
+ class="ms-3 action-icon-style" title="Cerrar sesión"
71
+ style="color: {{ branding.header_text_color }}; !important;">
67
72
  <i class="bi bi-box-arrow-right"></i>
68
73
  </a>
69
74
  </div>
@@ -192,13 +197,16 @@
192
197
  </script>
193
198
 
194
199
  <!-- Carga de los scripts JS externos después de definir las variables globales -->
195
- <script src="{{ url_for('static', filename='js/chat_onboarding.js', _external=True) }}"></script>
200
+ <script src="{{ url_for('static', filename='js/chat_onboarding_button.js', _external=True) }}"></script>
196
201
  <script src="{{ url_for('static', filename='js/chat_filepond.js', _external=True) }}"></script>
197
- <script src="{{ url_for('static', filename='js/chat_history.js', _external=True) }}"></script>
198
- <script src="{{ url_for('static', filename='js/chat_feedback.js', _external=True) }}"></script>
199
- <script src="{{ url_for('static', filename='js/chat_context_reload.js', _external=True) }}"></script><script src="{{ url_for('static', filename='js/chat_main.js', _external=True) }}"></script>
202
+ <script src="{{ url_for('static', filename='js/chat_history_button.js', _external=True) }}"></script>
203
+ <script src="{{ url_for('static', filename='js/chat_feedback_button.js', _external=True) }}"></script>
204
+ <script src="{{ url_for('static', filename='js/chat_reload_button.js', _external=True) }}"></script>
205
+ <script src="{{ url_for('static', filename='js/chat_main.js', _external=True) }}"></script>
206
+ <script src="{{ url_for('static', filename='js/chat_logout_button.js', _external=True) }}"></script>
207
+ <script src="{{ url_for('static', filename='js/chat_main.js', _external=True) }}"></script>
200
208
 
201
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
209
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
202
210
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
203
211
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
204
212
 
@@ -71,7 +71,7 @@
71
71
  {% block scripts %}
72
72
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
73
73
 
74
- <script src="{{ url_for('static', filename='js/chat_onboarding.js', _external=True) }}"></script>
74
+ <script src="{{ url_for('static', filename='js/chat_onboarding_button.js', _external=True) }}"></script>
75
75
  <script>
76
76
  (function() {
77
77
  const cardsData = {{ onboarding_cards | tojson }};
@@ -37,8 +37,8 @@ class ChangePasswordView(MethodView):
37
37
  email = self.serializer.loads(token, salt='password-reset', max_age=3600)
38
38
  except SignatureExpired as e:
39
39
  return render_template('forgot_password.html',
40
- branding=branding_data,
41
- alert_message="El enlace de cambio de contraseña ha expirado. Por favor, solicita uno nuevo.")
40
+ branding=branding_data,
41
+ alert_message="El enlace de cambio de contraseña ha expirado. Por favor, solicita uno nuevo.")
42
42
 
43
43
  return render_template('change_password.html',
44
44
  company_short_name=company_short_name,
@@ -15,22 +15,16 @@ class ExternalLoginView(BaseLoginView):
15
15
  Authenticates and then delegates the path decision (fast/slow) to the base class.
16
16
  """
17
17
  def post(self, company_short_name: str):
18
- data = request.get_json()
19
- if not data or 'user_identifier' not in data:
20
- return jsonify({"error": "Falta user_identifier"}), 400
18
+ # Authenticate the API call.
19
+ auth_result = self.auth_service.verify()
20
+ if not auth_result.get("success"):
21
+ return jsonify(auth_result), auth_result.get("status_code")
21
22
 
22
23
  company = self.profile_service.get_company_by_short_name(company_short_name)
23
24
  if not company:
24
25
  return jsonify({"error": "Empresa no encontrada"}), 404
25
26
 
26
- user_identifier = data.get('user_identifier')
27
- if not user_identifier:
28
- return jsonify({"error": "missing user_identifier"}), 404
29
-
30
- # 1. Authenticate the API call.
31
- auth_response = self.auth_service.verify()
32
- if not auth_response.get("success"):
33
- return jsonify(auth_response), 401
27
+ user_identifier = auth_result.get('user_identifier')
34
28
 
35
29
  # 2. Create the external user session.
36
30
  self.profile_service.create_external_user_profile_context(company, user_identifier)
@@ -15,24 +15,27 @@ import base64
15
15
  class FileStoreApiView(MethodView):
16
16
  @inject
17
17
  def __init__(self,
18
- iauthentication: AuthService,
18
+ auth_service: AuthService,
19
19
  doc_service: LoadDocumentsService,
20
20
  profile_repo: ProfileRepo,):
21
- self.iauthentication = iauthentication
21
+ self.auth_service = auth_service
22
22
  self.doc_service = doc_service
23
23
  self.profile_repo = profile_repo
24
24
 
25
25
  def post(self):
26
26
  try:
27
- req_data = request.get_json()
27
+ # 1. Authenticate the API request.
28
+ auth_result = self.auth_service.verify()
29
+ if not auth_result.get("success"):
30
+ return jsonify(auth_result), auth_result.get("status_code")
28
31
 
32
+ req_data = request.get_json()
29
33
  required_fields = ['company', 'filename', 'content']
30
34
  for field in required_fields:
31
35
  if field not in req_data:
32
36
  return jsonify({"error": f"El campo {field} es requerido"}), 400
33
37
 
34
38
  company_short_name = req_data.get('company', '')
35
- requested_name = req_data.get('username', 'external_user')
36
39
  filename = req_data.get('filename', False)
37
40
  base64_content = req_data.get('content', '')
38
41
  metadata = req_data.get('metadata', {})
@@ -42,11 +45,6 @@ class FileStoreApiView(MethodView):
42
45
  if not company:
43
46
  return jsonify({"error": f"La empresa {company_short_name} no existe"}), 400
44
47
 
45
- # get access credentials
46
- iaut = self.iauthentication.verify()
47
- if not iaut.get("success"):
48
- return jsonify(iaut), 401
49
-
50
48
  # get the file content from base64
51
49
  content = base64.b64decode(base64_content)
52
50