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.
- iatoolkit/__init__.py +2 -6
- iatoolkit/base_company.py +9 -29
- iatoolkit/cli_commands.py +1 -1
- iatoolkit/common/routes.py +96 -52
- iatoolkit/common/session_manager.py +2 -1
- iatoolkit/common/util.py +17 -27
- iatoolkit/company_registry.py +1 -2
- iatoolkit/iatoolkit.py +97 -53
- iatoolkit/infra/llm_client.py +15 -20
- iatoolkit/infra/llm_proxy.py +38 -10
- iatoolkit/infra/openai_adapter.py +1 -1
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +23 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +35 -10
- iatoolkit/repositories/profile_repo.py +3 -2
- iatoolkit/repositories/vs_repo.py +26 -20
- iatoolkit/services/auth_service.py +193 -0
- iatoolkit/services/branding_service.py +70 -25
- iatoolkit/services/company_context_service.py +155 -0
- iatoolkit/services/configuration_service.py +133 -0
- iatoolkit/services/dispatcher_service.py +80 -105
- iatoolkit/services/document_service.py +5 -2
- iatoolkit/services/embedding_service.py +146 -0
- iatoolkit/services/excel_service.py +30 -26
- iatoolkit/services/file_processor_service.py +4 -12
- iatoolkit/services/history_service.py +7 -16
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +18 -29
- iatoolkit/services/language_service.py +83 -0
- iatoolkit/services/load_documents_service.py +100 -113
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/profile_service.py +152 -76
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +208 -96
- iatoolkit/services/search_service.py +11 -4
- iatoolkit/services/sql_service.py +57 -25
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +72 -34
- iatoolkit/services/user_session_context_service.py +112 -54
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +135 -222
- iatoolkit/static/js/chat_onboarding_button.js +103 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +289 -210
- iatoolkit/static/styles/chat_modal.css +63 -77
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/query_main.prompt +5 -22
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +40 -20
- iatoolkit/templates/change_password.html +57 -36
- iatoolkit/templates/chat.html +180 -86
- iatoolkit/templates/chat_modals.html +138 -68
- iatoolkit/templates/error.html +44 -8
- iatoolkit/templates/forgot_password.html +40 -23
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +45 -0
- iatoolkit/templates/onboarding_shell.html +107 -0
- iatoolkit/templates/signup.html +63 -65
- iatoolkit/views/base_login_view.py +91 -0
- iatoolkit/views/change_password_view.py +56 -31
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/external_login_view.py +61 -28
- iatoolkit/views/{file_store_view.py → file_store_api_view.py} +10 -3
- iatoolkit/views/forgot_password_view.py +27 -21
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +50 -23
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +58 -0
- iatoolkit/views/login_simulation_view.py +93 -0
- iatoolkit/views/login_view.py +130 -37
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
- iatoolkit/views/signup_view.py +41 -36
- iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/verify_user_view.py +34 -29
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/METADATA +41 -23
- iatoolkit-0.71.2.dist-info/RECORD +122 -0
- iatoolkit-0.71.2.dist-info/licenses/LICENSE +21 -0
- iatoolkit/common/auth.py +0 -200
- iatoolkit/static/images/arrow_up.png +0 -0
- iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
- iatoolkit/static/images/logo_clinica.png +0 -0
- iatoolkit/static/images/logo_iatoolkit.png +0 -0
- iatoolkit/static/images/logo_maxxa.png +0 -0
- iatoolkit/static/images/logo_notaria.png +0 -0
- iatoolkit/static/images/logo_tarjeta.png +0 -0
- iatoolkit/static/images/logo_umayor.png +0 -0
- iatoolkit/static/images/upload.png +0 -0
- iatoolkit/static/js/chat_feedback.js +0 -115
- iatoolkit/static/js/chat_history.js +0 -117
- iatoolkit/static/styles/chat_info.css +0 -53
- iatoolkit/templates/header.html +0 -31
- iatoolkit/templates/home.html +0 -199
- iatoolkit/templates/login.html +0 -43
- iatoolkit/templates/test.html +0 -9
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/chat_view.py +0 -58
- iatoolkit/views/download_file_view.py +0 -58
- iatoolkit/views/external_chat_login_view.py +0 -95
- iatoolkit/views/history_view.py +0 -57
- iatoolkit/views/llmquery_view.py +0 -65
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit/views/user_feedback_view.py +0 -74
- iatoolkit-0.11.0.dist-info/RECORD +0 -110
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/top_level.txt +0 -0
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
$(document).ready(function () {
|
|
2
|
-
|
|
3
|
-
// Evento para enviar el feedback
|
|
4
|
-
$('#submit-feedback').on('click', async function() {
|
|
5
|
-
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>×</span></button>'; // Versión BS3/BS4
|
|
16
|
-
// --- FIN DE LA LÓGICA DE COMPATIBILIDAD ---
|
|
17
|
-
|
|
18
|
-
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);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const activeStars = $('.star.active').length;
|
|
30
|
-
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);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
submitButton.prop('disabled', true);
|
|
42
|
-
submitButton.html('<i class="bi bi-send me-1 icon-spaced"></i>Enviando...');
|
|
43
|
-
|
|
44
|
-
const response = await sendFeedback(feedbackText);
|
|
45
|
-
|
|
46
|
-
$('#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
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Evento para abrir el modal de feedback
|
|
56
|
-
$('#send-feedback-button').on('click', function() {
|
|
57
|
-
$('#submit-feedback').prop('disabled', false);
|
|
58
|
-
$('#submit-feedback').html('<i class="bi bi-send me-1 icon-spaced"></i>Enviar');
|
|
59
|
-
$('.star').removeClass('active hover-active'); // Resetea estrellas
|
|
60
|
-
$('#feedback-text').val(''); // Limpia texto
|
|
61
|
-
$('.modal-body .alert').remove(); // Quita alertas previas
|
|
62
|
-
$('#feedbackModal').modal('show');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Evento que se dispara DESPUÉS de que el modal se ha ocultado
|
|
66
|
-
$('#feedbackModal').on('hidden.bs.modal', function () {
|
|
67
|
-
$('#feedback-text').val('');
|
|
68
|
-
$('.modal-body .alert').remove();
|
|
69
|
-
$('.star').removeClass('active');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Función para el sistema de estrellas
|
|
73
|
-
window.gfg = function(rating) {
|
|
74
|
-
$('.star').removeClass('active');
|
|
75
|
-
$('.star').each(function(index) {
|
|
76
|
-
if (index < rating) {
|
|
77
|
-
$(this).addClass('active');
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
$('.star').hover(
|
|
83
|
-
function() {
|
|
84
|
-
const rating = $(this).data('rating');
|
|
85
|
-
$('.star').removeClass('hover-active');
|
|
86
|
-
$('.star').each(function(index) {
|
|
87
|
-
if ($(this).data('rating') <= rating) {
|
|
88
|
-
$(this).addClass('hover-active');
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
},
|
|
92
|
-
function() {
|
|
93
|
-
$('.star').removeClass('hover-active');
|
|
94
|
-
}
|
|
95
|
-
);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const sendFeedback = async function(message) {
|
|
99
|
-
const activeStars = $('.star.active').length;
|
|
100
|
-
const data = {
|
|
101
|
-
"external_user_id": window.externalUserId,
|
|
102
|
-
"message": message,
|
|
103
|
-
"rating": activeStars,
|
|
104
|
-
"space": "spaces/AAQAupQldd4", // Este valor podría necesitar ser dinámico
|
|
105
|
-
"type": "MESSAGE_TRIGGER"
|
|
106
|
-
};
|
|
107
|
-
try {
|
|
108
|
-
// Asumiendo que callLLMAPI está definido globalmente en otro archivo (ej. chat_main.js)
|
|
109
|
-
const responseData = await callLLMAPI('/feedback', data, "POST");
|
|
110
|
-
return responseData;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.error("Error al enviar feedback:", error);
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
$(document).ready(function () {
|
|
2
|
-
// Evento para abrir el modal de historial
|
|
3
|
-
$('#history-button').on('click', function() {
|
|
4
|
-
loadHistory();
|
|
5
|
-
$('#historyModal').modal('show');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
// Variables globales para el historial
|
|
9
|
-
let historyData = [];
|
|
10
|
-
|
|
11
|
-
// Función para cargar el historial
|
|
12
|
-
async function loadHistory() {
|
|
13
|
-
const historyLoading = $('#history-loading');
|
|
14
|
-
const historyError = $('#history-error');
|
|
15
|
-
const historyContent = $('#history-content');
|
|
16
|
-
|
|
17
|
-
// Mostrar loading
|
|
18
|
-
historyLoading.show();
|
|
19
|
-
historyError.hide();
|
|
20
|
-
historyContent.hide();
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const data = {
|
|
24
|
-
external_user_id: window.externalUserId
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const responseData = await callLLMAPI("/history", data, "POST");
|
|
28
|
-
|
|
29
|
-
if (responseData && responseData.history) {
|
|
30
|
-
// Guardar datos globalmente
|
|
31
|
-
historyData = responseData.history;
|
|
32
|
-
|
|
33
|
-
// Mostrar todos los datos
|
|
34
|
-
displayAllHistory();
|
|
35
|
-
|
|
36
|
-
// Mostrar contenido
|
|
37
|
-
historyContent.show();
|
|
38
|
-
} else {
|
|
39
|
-
throw new Error('No se recibieron datos del historial');
|
|
40
|
-
}
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error("Error al cargar historial:", error);
|
|
43
|
-
const errorHtml = `
|
|
44
|
-
<div class="alert alert-branded-danger alert-dismissible show" role="alert">
|
|
45
|
-
<strong>Error al cargar el historial:</strong> ${error.message}
|
|
46
|
-
<button type="button" class="close" data-dismiss="alert">
|
|
47
|
-
<span>×</span>
|
|
48
|
-
</button>
|
|
49
|
-
</div>
|
|
50
|
-
`;
|
|
51
|
-
historyError.html(errorHtml).show();
|
|
52
|
-
} finally {
|
|
53
|
-
historyLoading.hide();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Función para mostrar todo el historial
|
|
58
|
-
function displayAllHistory() {
|
|
59
|
-
const historyTableBody = $('#history-table-body');
|
|
60
|
-
|
|
61
|
-
// Limpiar tabla
|
|
62
|
-
historyTableBody.empty();
|
|
63
|
-
|
|
64
|
-
// Filtrar solo consultas que son strings simples (no objetos JSON)
|
|
65
|
-
const filteredHistory = historyData.filter(item => {
|
|
66
|
-
try {
|
|
67
|
-
// Intentar parsear como JSON
|
|
68
|
-
const parsed = JSON.parse(item.query);
|
|
69
|
-
// Si se puede parsear y es un objeto, filtrarlo
|
|
70
|
-
return false;
|
|
71
|
-
} catch (e) {
|
|
72
|
-
// Si no se puede parsear, es un string simple, incluirlo
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Poblar tabla solo con las consultas filtradas
|
|
78
|
-
filteredHistory.forEach((item, index) => {
|
|
79
|
-
const row = $(`
|
|
80
|
-
<tr>
|
|
81
|
-
<td>${index + 1}</td>
|
|
82
|
-
<td>${formatDate(item.created_at)}</td>
|
|
83
|
-
<td class="query-cell" style="cursor: pointer;" title="Haz clic para copiar esta consulta al chat">${item.query}</td>
|
|
84
|
-
</tr>
|
|
85
|
-
`);
|
|
86
|
-
historyTableBody.append(row);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Agregar evento de clic a las celdas de consulta
|
|
90
|
-
historyTableBody.on('click', '.query-cell', function() {
|
|
91
|
-
const queryText = $(this).text();
|
|
92
|
-
|
|
93
|
-
// Copiar el texto al textarea del chat
|
|
94
|
-
$('#question').val(queryText);
|
|
95
|
-
|
|
96
|
-
// Cerrar el modal
|
|
97
|
-
$('#historyModal').modal('hide');
|
|
98
|
-
|
|
99
|
-
// Hacer focus en el textarea para que el usuario pueda editar si lo desea
|
|
100
|
-
$('#question').focus();
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Función para formatear fecha
|
|
105
|
-
function formatDate(dateString) {
|
|
106
|
-
const date = new Date(dateString);
|
|
107
|
-
return date.toLocaleDateString('es-CL', {
|
|
108
|
-
day: '2-digit',
|
|
109
|
-
month: '2-digit',
|
|
110
|
-
year: 'numeric',
|
|
111
|
-
hour: '2-digit',
|
|
112
|
-
minute: '2-digit'
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
});
|
|
117
|
-
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Estilo para el encabezado de la tabla (thead).
|
|
3
|
-
* Se usa un fondo gris claro, texto en mayúsculas y
|
|
4
|
-
* un espaciado de letras para un look más profesional.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
.table{
|
|
8
|
-
background-color: white;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.title-info-page{
|
|
12
|
-
color: #495057;
|
|
13
|
-
font-size: 24px;
|
|
14
|
-
font-weight: bold;
|
|
15
|
-
margin-bottom: 10px;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.table thead th {
|
|
19
|
-
background-color: #f8f9fa;
|
|
20
|
-
color: #495057;
|
|
21
|
-
text-transform: uppercase;
|
|
22
|
-
letter-spacing: 0.5px;
|
|
23
|
-
border-bottom-width: 2px;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/*
|
|
27
|
-
* Columna 1: Ancho mínimo, sin salto de línea y en negrita.
|
|
28
|
-
*/
|
|
29
|
-
.table th:nth-child(1),
|
|
30
|
-
.table td:nth-child(1) {
|
|
31
|
-
width: 1%;
|
|
32
|
-
white-space: nowrap;
|
|
33
|
-
font-weight: bold;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/*
|
|
37
|
-
* Columna 2: Ancho mínimo y sin salto de línea.
|
|
38
|
-
*/
|
|
39
|
-
.table th:nth-child(2),
|
|
40
|
-
.table td:nth-child(2) {
|
|
41
|
-
width: 1%;
|
|
42
|
-
white-space: nowrap;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/*
|
|
46
|
-
* Columna 3 (Descripción): Limita su ancho máximo
|
|
47
|
-
* y ajusta el texto largo para que no se desborde.
|
|
48
|
-
*/
|
|
49
|
-
.table th:nth-child(3),
|
|
50
|
-
.table td:nth-child(3) {
|
|
51
|
-
max-width: 450px;
|
|
52
|
-
word-wrap: break-word;
|
|
53
|
-
}
|
iatoolkit/templates/header.html
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{% if not external_login %}
|
|
2
|
-
<header class="modern-header">
|
|
3
|
-
{% if not is_mobile or not user %}
|
|
4
|
-
<div class="logo-section">
|
|
5
|
-
<a href="{{ url_for('chat', company_short_name=company_short_name) }}" title="Ir a Inicio">
|
|
6
|
-
{% if company_short_name %}
|
|
7
|
-
<img src="{{ url_for('static', filename='images/' + company.logo_file) }}" alt="Logo de la empresa">
|
|
8
|
-
{% else %}
|
|
9
|
-
<img src="{{ url_for('static', filename='images/logo_iatoolkit.png') }}" alt="Logo predeterminado">
|
|
10
|
-
{% endif %}
|
|
11
|
-
</a>
|
|
12
|
-
</div>
|
|
13
|
-
{% endif %}
|
|
14
|
-
{% if user and user_company == company_short_name %}
|
|
15
|
-
<div class="d-flex ms-auto align-items-center">
|
|
16
|
-
<!-- Nombres del usuario y empresa -->
|
|
17
|
-
<div class="d-flex flex-column {% if not is_mobile %} align-items-end {% endif %} me-3">
|
|
18
|
-
<span class="text-white fw-semibold">
|
|
19
|
-
{{ user.email }}
|
|
20
|
-
</span>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<!-- Icono de cerrar sesión -->
|
|
24
|
-
<a href="{{ url_for('logout', company_short_name=company_short_name) }}"
|
|
25
|
-
| class="text-white fs-4 ms-3" title="Cerrar sesión">
|
|
26
|
-
<i class="bi bi-box-arrow-right"></i>
|
|
27
|
-
</a>
|
|
28
|
-
</div>
|
|
29
|
-
{% endif %}
|
|
30
|
-
</header>
|
|
31
|
-
{% endif %}
|
iatoolkit/templates/home.html
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
|
|
3
|
-
{% block title %}Inicio - IAToolkit{% endblock %}
|
|
4
|
-
|
|
5
|
-
{% block content %}
|
|
6
|
-
|
|
7
|
-
<!-- Contenido principal con espaciado adicional respecto al header -->
|
|
8
|
-
<div class="row flex-fill mt-5 flex-wrap">
|
|
9
|
-
|
|
10
|
-
{% if not user or user_company != company_short_name %}
|
|
11
|
-
<!-- Sección de login (se coloca primero en el HTML para mobile) -->
|
|
12
|
-
<div class="col-12 col-lg-5 offset-lg-1">
|
|
13
|
-
<div class="border rounded p-4 shadow-sm bg-light">
|
|
14
|
-
<h4 class="text-muted fw-semibold text-start mb-3">login integrado (IAToolkit)</h4>
|
|
15
|
-
<form id="login-form"
|
|
16
|
-
action="{{ url_for('home', company_short_name=company_short_name) }}"
|
|
17
|
-
method="post">
|
|
18
|
-
<div class="mb-3">
|
|
19
|
-
<label for="company_short_name" class="form-label d-block text-muted">Empresa</label>
|
|
20
|
-
<select id="company_short_name" name="company_short_name" class="form-select" required>
|
|
21
|
-
<option value="" disabled selected>Selecciona una empresa</option>
|
|
22
|
-
{% for company in companies %}
|
|
23
|
-
<option value="{{ company.short_name }}"
|
|
24
|
-
{% if company.short_name == company_short_name %}selected{% endif %}>
|
|
25
|
-
{{ company.short_name }}
|
|
26
|
-
</option>
|
|
27
|
-
{% endfor %}
|
|
28
|
-
</select>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<div class="mb-3">
|
|
32
|
-
<label for="email" class="form-label d-block text-muted">Correo Electrónico</label>
|
|
33
|
-
<input type="email" id="email" name="email" class="form-control" required>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="mb-3">
|
|
36
|
-
<label for="password" class="form-label d-block text-muted">Contraseña</label>
|
|
37
|
-
<input type="password" id="password" name="password" class="form-control" required>
|
|
38
|
-
</div>
|
|
39
|
-
<button type="submit" class="btn btn-primary w-100">
|
|
40
|
-
Iniciar Sesión</button>
|
|
41
|
-
</form>
|
|
42
|
-
|
|
43
|
-
<div class="text-center mt-3">
|
|
44
|
-
<a href="{% if company_short_name %}{{ url_for('signup', company_short_name=company_short_name) }}{% else %}#{% endif %}"
|
|
45
|
-
id="signup-link"
|
|
46
|
-
class="btn btn-outline-primary w-100">Registrarse</a>
|
|
47
|
-
</div>
|
|
48
|
-
<div class="text-center mt-3">
|
|
49
|
-
<a href="{{ url_for('forgot_password', company_short_name=company_short_name) }}" class="text-decoration-none text-muted fw-semibold">
|
|
50
|
-
¿Olvidaste tu contraseña?
|
|
51
|
-
</a>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
{% endif %}
|
|
56
|
-
|
|
57
|
-
<!-- Sección de JWT -->
|
|
58
|
-
<div class="col-12 col-lg-5 offset-lg-1">
|
|
59
|
-
<div class="border rounded p-4 shadow-sm bg-light">
|
|
60
|
-
<h4 class="text-muted fw-semibold text-start mb-3">login externo (api-key)</h4>
|
|
61
|
-
<form id="jwt-form" method="post">
|
|
62
|
-
<div class="mb-3">
|
|
63
|
-
<label for="company_name" class="form-label d-block text-muted">Empresa</label>
|
|
64
|
-
<select id="company_name" name="company_short_name" class="form-select" required>
|
|
65
|
-
<option value="" disabled selected>Selecciona una empresa</option>
|
|
66
|
-
{% for company in companies %}
|
|
67
|
-
{% if company.allow_jwt %}
|
|
68
|
-
<option value="{{ company.short_name }}"> {{ company.short_name }}
|
|
69
|
-
</option>
|
|
70
|
-
{% endif %}
|
|
71
|
-
{% endfor %}
|
|
72
|
-
</select>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div class="mb-3">
|
|
76
|
-
<label for="external_user_id" class="form-label d-block text-muted">External user ID</label>
|
|
77
|
-
<input type="text" id="external_user_id" name="external_user_id" class="form-control" required>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<button type="button"
|
|
81
|
-
id="initiateJwtChatButton"
|
|
82
|
-
class="ml-5 btn btn-primary">
|
|
83
|
-
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
|
84
|
-
Iniciar Sesión JWT
|
|
85
|
-
</button>
|
|
86
|
-
</form>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
{% endblock %}
|
|
93
|
-
|
|
94
|
-
{% block scripts %}
|
|
95
|
-
|
|
96
|
-
<script>
|
|
97
|
-
// Variables pasadas desde Flask (HomeView)
|
|
98
|
-
const API_KEY = "{{ api_key|safe }}";
|
|
99
|
-
|
|
100
|
-
$(document).ready(function () {
|
|
101
|
-
// Función para actualizar el enlace de "Registrarse" y el action del formulario "Iniciar Sesión"
|
|
102
|
-
function updateLinksAndForm() {
|
|
103
|
-
const selectedCompany = $('#company_short_name').val(); // Obtenemos el valor del select
|
|
104
|
-
|
|
105
|
-
// Actualizar enlace "Registrarse"
|
|
106
|
-
if (selectedCompany && selectedCompany.trim() !== '') {
|
|
107
|
-
const signupUrl = '/' + selectedCompany + '/signup';
|
|
108
|
-
$('#signup-link').attr('href', signupUrl); // Actualizamos el href del botón "Registrarse"
|
|
109
|
-
} else {
|
|
110
|
-
$('#signup-link').attr('href', '#'); // Enlace a "#" si no hay empresa seleccionada
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Actualizar action del formulario "Iniciar Sesión"
|
|
114
|
-
if (selectedCompany && selectedCompany.trim() !== '') {
|
|
115
|
-
const loginAction = '/' + selectedCompany + '/login';
|
|
116
|
-
$('#login-form').attr('action', loginAction); // Actualizamos la URL del form
|
|
117
|
-
} else {
|
|
118
|
-
$('#login-form').attr('action', '#'); // URL genérica si no hay selección
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Actualizamos al cargar la página
|
|
123
|
-
updateLinksAndForm();
|
|
124
|
-
|
|
125
|
-
// Escuchamos el evento de cambio en el dropdown para actualizar dinámicamente
|
|
126
|
-
$('#company_short_name').on('change', function () {
|
|
127
|
-
updateLinksAndForm();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Interceptamos el click en "Registrarse"
|
|
131
|
-
$('#signup-link').on('click', function (e) {
|
|
132
|
-
const selectedCompany = $('#company_short_name').val();
|
|
133
|
-
|
|
134
|
-
if (!selectedCompany || selectedCompany.trim() === '') {
|
|
135
|
-
e.preventDefault(); // evitar navegación al #
|
|
136
|
-
Swal.fire({
|
|
137
|
-
icon: 'warning',
|
|
138
|
-
title: 'Empresa no seleccionada',
|
|
139
|
-
text: 'Por favor, selecciona una empresa antes de registrarte.'
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Event listener para el botón de "Abrir Chat (JWT)"
|
|
145
|
-
$('#initiateJwtChatButton').on('click', function() {
|
|
146
|
-
|
|
147
|
-
const selectedCompany = $('#company_name').val();
|
|
148
|
-
const externalUserId = $('#external_user_id').val();
|
|
149
|
-
|
|
150
|
-
if (!selectedCompany || !externalUserId.trim()) {
|
|
151
|
-
Swal.fire({ icon: 'warning', title: 'Campos Requeridos', text: 'Por favor, selecciona una empresa e ingresa un ID de usuario.' });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (!API_KEY || API_KEY.includes("defecto")) {
|
|
155
|
-
Swal.fire({ icon: 'error', title: 'Error de Configuración', text: 'La API Key de la aplicación no está disponible.' });
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const $button = $(this);
|
|
160
|
-
const $spinner = $button.find('.spinner-border');
|
|
161
|
-
$button.prop('disabled', true);
|
|
162
|
-
$spinner.removeClass('d-none');
|
|
163
|
-
|
|
164
|
-
fetch(`/${selectedCompany}/chat_login`, {
|
|
165
|
-
method: 'POST',
|
|
166
|
-
headers: {
|
|
167
|
-
'Content-Type': 'application/json',
|
|
168
|
-
'Authorization': `Bearer ${API_KEY}`
|
|
169
|
-
},
|
|
170
|
-
body: JSON.stringify({
|
|
171
|
-
external_user_id: externalUserId
|
|
172
|
-
})
|
|
173
|
-
})
|
|
174
|
-
.then(async response => {
|
|
175
|
-
if (response.ok) { // Si el status es 200, la respuesta es el HTML
|
|
176
|
-
return response.text();
|
|
177
|
-
} else { // Si hay un error, el cuerpo es JSON
|
|
178
|
-
const errorData = await response.json();
|
|
179
|
-
throw new Error(errorData.error || 'Ocurrió un error desconocido.');
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
.then(htmlContent => {
|
|
183
|
-
// Éxito: Abrimos el HTML que nos devolvió el servidor en una nueva pestaña.
|
|
184
|
-
const newTab = window.open();
|
|
185
|
-
newTab.document.write(htmlContent);
|
|
186
|
-
newTab.document.close();
|
|
187
|
-
})
|
|
188
|
-
.catch(error => {
|
|
189
|
-
Swal.fire({ icon: 'error', title: 'Error de Inicio de Sesión', text: error.message });
|
|
190
|
-
})
|
|
191
|
-
.finally(() => {
|
|
192
|
-
$button.prop('disabled', false);
|
|
193
|
-
$spinner.addClass('d-none');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
</script>
|
|
199
|
-
{% endblock %}
|
iatoolkit/templates/login.html
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
|
|
3
|
-
{% block title %}Inicio de Sesión{% endblock %}
|
|
4
|
-
|
|
5
|
-
{% block content %}
|
|
6
|
-
<div class="container d-flex justify-content-center align-items-center vh-100">
|
|
7
|
-
<div class="col-11 col-md-6 col-lg-4 border rounded p-3 shadow-sm">
|
|
8
|
-
<h4 class="text-muted fw-semibold text-start mb-3">Iniciar Sesión - {{ company.name }}
|
|
9
|
-
</h4>
|
|
10
|
-
<form action="{{ url_for('login', company_short_name=company_short_name) }}"
|
|
11
|
-
method="post">
|
|
12
|
-
<div class="mb-3">
|
|
13
|
-
<label for="email" class="form-label text-muted">Correo Electrónico</label>
|
|
14
|
-
<input type="email" id="email" name="email"
|
|
15
|
-
class="form-control" required
|
|
16
|
-
value="{{ form_data.email if form_data else '' }}">
|
|
17
|
-
</div>
|
|
18
|
-
<div class="mb-3">
|
|
19
|
-
<label for="password" class="form-label">Contraseña</label>
|
|
20
|
-
<input type="password" id="password" name="password"
|
|
21
|
-
class="form-control" required
|
|
22
|
-
value="{{ form_data.password if form_data else '' }}">
|
|
23
|
-
</div>
|
|
24
|
-
<button type="submit" class="btn btn-primary w-100">Iniciar Sesión</button>
|
|
25
|
-
|
|
26
|
-
<p class="text-muted text-start mt-3" style="text-align: justify;">
|
|
27
|
-
Ingresa tus credenciales para acceder a tu cuenta. Si olvidaste tu contraseña,
|
|
28
|
-
puedes recuperarla fácilmente a través del siguiente enlace.
|
|
29
|
-
</p>
|
|
30
|
-
<div class="text-center mt-3">
|
|
31
|
-
<a href="{{ url_for('signup', company_short_name=company_short_name) }}">
|
|
32
|
-
¿No tienes cuenta? Regístrate
|
|
33
|
-
</a>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="text-center mt-3">
|
|
36
|
-
<a href="{{ url_for('forgot_password', company_short_name=company_short_name) }}">
|
|
37
|
-
¿Olvidaste tu contraseña?
|
|
38
|
-
</a>
|
|
39
|
-
</div>
|
|
40
|
-
</form>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
{% endblock %}
|
iatoolkit/templates/test.html
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Product: IAToolkit
|
|
3
|
-
#
|
|
4
|
-
# IAToolkit is open source software.
|
|
5
|
-
|
|
6
|
-
from flask import request, jsonify, current_app
|
|
7
|
-
from flask.views import MethodView
|
|
8
|
-
from injector import inject
|
|
9
|
-
import logging
|
|
10
|
-
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
11
|
-
from iatoolkit.services.jwt_service import JWTService
|
|
12
|
-
from typing import Optional
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# Necesitaremos JWT_EXPIRATION_SECONDS_CHAT de la configuración de la app
|
|
16
|
-
# Se podría inyectar o acceder globalmente.
|
|
17
|
-
|
|
18
|
-
class ChatTokenRequestView(MethodView):
|
|
19
|
-
@inject
|
|
20
|
-
def __init__(self, profile_repo: ProfileRepo, jwt_service: JWTService):
|
|
21
|
-
self.profile_repo = profile_repo
|
|
22
|
-
self.jwt_service = jwt_service
|
|
23
|
-
|
|
24
|
-
def _authenticate_requesting_company_via_api_key(self) -> tuple[
|
|
25
|
-
Optional[int], Optional[str], Optional[tuple[dict, int]]]:
|
|
26
|
-
"""
|
|
27
|
-
Autentica a la compañía que solicita el token JWT usando su API Key.
|
|
28
|
-
Retorna (company_id, company_short_name, None) en éxito.
|
|
29
|
-
Retorna (None, None, (error_json, status_code)) en fallo.
|
|
30
|
-
"""
|
|
31
|
-
api_key_header = request.headers.get('Authorization')
|
|
32
|
-
if not api_key_header or not api_key_header.startswith('Bearer '):
|
|
33
|
-
return None, None, ({"error": "API Key faltante o mal formada en el header Authorization"}, 401)
|
|
34
|
-
|
|
35
|
-
api_key_value = api_key_header.split('Bearer ')[1]
|
|
36
|
-
try:
|
|
37
|
-
api_key_entry = self.profile_repo.get_active_api_key_entry(api_key_value)
|
|
38
|
-
if not api_key_entry:
|
|
39
|
-
return None, None, ({"error": "API Key inválida o inactiva"}, 401)
|
|
40
|
-
|
|
41
|
-
# api_key_entry.company ya está cargado por joinedload en get_active_api_key_entry
|
|
42
|
-
if not api_key_entry.company: # Sanity check
|
|
43
|
-
logging.error(
|
|
44
|
-
f"ChatTokenRequest: API Key {api_key_value[:5]}... no tiene compañía asociada a pesar de ser válida.")
|
|
45
|
-
return None, None, ({"error": "Error interno del servidor al verificar API Key"}, 500)
|
|
46
|
-
|
|
47
|
-
return api_key_entry.company_id, api_key_entry.company.short_name, None
|
|
48
|
-
|
|
49
|
-
except Exception as e:
|
|
50
|
-
logging.exception(f"ChatTokenRequest: Error interno durante validación de API Key: {e}")
|
|
51
|
-
return None, None, ({"error": "Error interno del servidor al validar API Key"}, 500)
|
|
52
|
-
|
|
53
|
-
def post(self):
|
|
54
|
-
"""
|
|
55
|
-
Genera un JWT para una sesión de chat.
|
|
56
|
-
Autenticado por API Key de la empresa.
|
|
57
|
-
Requiere JSON body:
|
|
58
|
-
{"company_short_name": "target_company_name",
|
|
59
|
-
"external_user_id": "user_abc"
|
|
60
|
-
}
|
|
61
|
-
"""
|
|
62
|
-
# only requests with valid api-key are allowed
|
|
63
|
-
auth_company_id, auth_company_short_name, error = self._authenticate_requesting_company_via_api_key()
|
|
64
|
-
if error:
|
|
65
|
-
return jsonify(error[0]), error[1]
|
|
66
|
-
|
|
67
|
-
# get the json fields from the request body
|
|
68
|
-
data = request.get_json()
|
|
69
|
-
if not data:
|
|
70
|
-
return jsonify({"error": "Cuerpo de la solicitud JSON faltante"}), 400
|
|
71
|
-
|
|
72
|
-
target_company_short_name = data.get('company_short_name')
|
|
73
|
-
external_user_id = data.get('external_user_id')
|
|
74
|
-
|
|
75
|
-
if not target_company_short_name or not external_user_id:
|
|
76
|
-
return jsonify(
|
|
77
|
-
{"error": "Faltan 'company_short_name' o 'external_user_id' en el cuerpo de la solicitud"}), 401
|
|
78
|
-
|
|
79
|
-
# Verificar que la API Key usada pertenezca a la empresa para la cual se solicita el token
|
|
80
|
-
if auth_company_short_name != target_company_short_name:
|
|
81
|
-
return jsonify({
|
|
82
|
-
"error": f"API Key no autorizada para generar tokens para la compañía '{target_company_short_name}'"}), 403
|
|
83
|
-
|
|
84
|
-
jwt_expiration_seconds = current_app.config.get('JWT_EXPIRATION_SECONDS_CHAT', 3600)
|
|
85
|
-
|
|
86
|
-
# Aquí, auth_company_id es el ID de la compañía para la que se generará el token.
|
|
87
|
-
# auth_company_short_name es su nombre corto.
|
|
88
|
-
token = self.jwt_service.generate_chat_jwt(
|
|
89
|
-
company_id=auth_company_id,
|
|
90
|
-
company_short_name=auth_company_short_name, # Usamos el short_name autenticado
|
|
91
|
-
external_user_id=external_user_id,
|
|
92
|
-
expires_delta_seconds=jwt_expiration_seconds
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
if token:
|
|
96
|
-
return jsonify({"chat_jwt": token}), 200
|
|
97
|
-
else:
|
|
98
|
-
return jsonify({"error": "No se pudo generar el token de chat"}), 500
|