iatoolkit 1.7.0__py3-none-any.whl → 1.15.3__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 +1 -1
- iatoolkit/common/routes.py +16 -3
- iatoolkit/common/util.py +8 -123
- iatoolkit/core.py +1 -0
- iatoolkit/infra/connectors/file_connector.py +10 -2
- iatoolkit/infra/connectors/google_drive_connector.py +3 -0
- iatoolkit/infra/connectors/local_file_connector.py +3 -0
- iatoolkit/infra/connectors/s3_connector.py +24 -1
- iatoolkit/infra/llm_providers/deepseek_adapter.py +17 -1
- iatoolkit/infra/llm_providers/gemini_adapter.py +117 -18
- iatoolkit/infra/llm_providers/openai_adapter.py +175 -18
- iatoolkit/infra/llm_response.py +13 -0
- iatoolkit/locales/en.yaml +82 -4
- iatoolkit/locales/es.yaml +79 -4
- iatoolkit/repositories/llm_query_repo.py +51 -18
- iatoolkit/repositories/models.py +16 -7
- iatoolkit/services/company_context_service.py +294 -133
- iatoolkit/services/configuration_service.py +140 -121
- iatoolkit/services/dispatcher_service.py +1 -4
- iatoolkit/services/knowledge_base_service.py +26 -4
- iatoolkit/services/llm_client_service.py +58 -2
- iatoolkit/services/prompt_service.py +251 -164
- iatoolkit/services/query_service.py +37 -18
- iatoolkit/services/storage_service.py +92 -0
- iatoolkit/static/js/chat_filepond.js +188 -63
- iatoolkit/static/js/chat_main.js +105 -52
- iatoolkit/static/styles/chat_iatoolkit.css +96 -0
- iatoolkit/system_prompts/query_main.prompt +24 -41
- iatoolkit/templates/chat.html +15 -6
- iatoolkit/views/base_login_view.py +1 -1
- iatoolkit/views/categories_api_view.py +111 -0
- iatoolkit/views/chat_view.py +1 -1
- iatoolkit/views/configuration_api_view.py +1 -1
- iatoolkit/views/login_view.py +1 -1
- iatoolkit/views/prompt_api_view.py +88 -7
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/METADATA +1 -1
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/RECORD +41 -39
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/WHEEL +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import uuid
|
|
8
|
+
import logging
|
|
9
|
+
import mimetypes
|
|
10
|
+
import os
|
|
11
|
+
from injector import inject
|
|
12
|
+
from typing import Dict
|
|
13
|
+
from iatoolkit.infra.connectors.s3_connector import S3Connector
|
|
14
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StorageService:
|
|
18
|
+
"""
|
|
19
|
+
High level service for managing assets storage (images generated, attachments, etc).
|
|
20
|
+
Provides abstraction for file decoding and naming.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@inject
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.connector = self._init_connector()
|
|
26
|
+
|
|
27
|
+
def _init_connector(self) -> S3Connector:
|
|
28
|
+
# We configure S3 directly using environment variables
|
|
29
|
+
bucket = os.getenv("S3_BUCKET_NAME", "iatoolkit-assets")
|
|
30
|
+
|
|
31
|
+
auth = {
|
|
32
|
+
'aws_access_key_id': os.getenv('AWS_ACCESS_KEY_ID'),
|
|
33
|
+
'aws_secret_access_key': os.getenv('AWS_SECRET_ACCESS_KEY'),
|
|
34
|
+
'region_name': os.getenv('AWS_REGION', 'us-east-1')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return S3Connector(
|
|
38
|
+
bucket=bucket,
|
|
39
|
+
prefix="", # Empty prefix to allow full control over keys
|
|
40
|
+
folder="",
|
|
41
|
+
auth=auth
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def store_generated_image(self, company_short_name: str, base64_data: str, mime_type: str) -> Dict[str, str]:
|
|
45
|
+
"""
|
|
46
|
+
Guarda una imagen generada por LLM (Base64) en el storage.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dict con:
|
|
50
|
+
- 'storage_key': La ruta interna (para guardar en BD)
|
|
51
|
+
- 'url': La URL firmada (para devolver al frontend inmediatamente)
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
# 1. Decode Base64
|
|
55
|
+
# Sometimes the string comes with header 'data:image/png;base64,...', clean it
|
|
56
|
+
if "base64," in base64_data:
|
|
57
|
+
base64_data = base64_data.split("base64,")[1]
|
|
58
|
+
|
|
59
|
+
image_bytes = base64.b64decode(base64_data)
|
|
60
|
+
|
|
61
|
+
# 2. Generate unique name
|
|
62
|
+
ext = mimetypes.guess_extension(mime_type) or ".png"
|
|
63
|
+
filename = f"{uuid.uuid4()}{ext}"
|
|
64
|
+
|
|
65
|
+
# 3. Define folder structure: companies/{company}/generated/{filename}
|
|
66
|
+
storage_key = f"companies/{company_short_name}/generated_images/{filename}"
|
|
67
|
+
|
|
68
|
+
# 4. Upload
|
|
69
|
+
self.connector.upload_file(
|
|
70
|
+
file_path=storage_key,
|
|
71
|
+
content=image_bytes,
|
|
72
|
+
content_type=mime_type
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
logging.info(f"Generated image saved at: {storage_key}")
|
|
76
|
+
|
|
77
|
+
# 5. Generate temporary URL
|
|
78
|
+
url = self.connector.generate_presigned_url(storage_key)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"storage_key": storage_key,
|
|
82
|
+
"url": url
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
error_msg = f"Error saving image to Storage: {str(e)}"
|
|
87
|
+
logging.error(error_msg)
|
|
88
|
+
raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR, error_msg)
|
|
89
|
+
|
|
90
|
+
def get_public_url(self, storage_key: str) -> str:
|
|
91
|
+
"""Gets a fresh signed URL for an existing resource."""
|
|
92
|
+
return self.connector.generate_presigned_url(storage_key)
|
|
@@ -1,85 +1,210 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
2
|
+
// 1. Register FilePond Plugins
|
|
3
|
+
// Ensure plugin scripts are loaded in your base layout or template
|
|
4
|
+
if (typeof FilePondPluginFileEncode !== 'undefined') {
|
|
5
|
+
FilePond.registerPlugin(FilePondPluginFileEncode);
|
|
6
|
+
}
|
|
7
|
+
if (typeof FilePondPluginFileValidateSize !== 'undefined') {
|
|
8
|
+
FilePond.registerPlugin(FilePondPluginFileValidateSize);
|
|
9
|
+
}
|
|
10
|
+
if (typeof FilePondPluginFileValidateType !== 'undefined') {
|
|
11
|
+
FilePond.registerPlugin(FilePondPluginFileValidateType);
|
|
12
|
+
}
|
|
13
|
+
if (typeof FilePondPluginImagePreview !== 'undefined') {
|
|
14
|
+
FilePond.registerPlugin(FilePondPluginImagePreview);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 2. Create FilePond instance on the hidden input
|
|
18
|
+
const inputElement = document.querySelector('input.filepond');
|
|
19
|
+
|
|
20
|
+
// FilePond base configuration
|
|
21
|
+
const filePond = FilePond.create(inputElement, {
|
|
12
22
|
allowMultiple: true,
|
|
13
|
-
|
|
23
|
+
maxFiles: 5,
|
|
24
|
+
maxFileSize: '30MB',
|
|
25
|
+
|
|
26
|
+
// Extensive list of accepted types (Images, PDF, Text, Excel, Word)
|
|
27
|
+
acceptedFileTypes: [
|
|
28
|
+
'image/*',
|
|
29
|
+
'application/pdf',
|
|
30
|
+
'text/plain',
|
|
31
|
+
'text/csv',
|
|
32
|
+
'application/vnd.ms-excel', // .xls
|
|
33
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
|
34
|
+
'application/msword', // .doc
|
|
35
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' // .docx
|
|
36
|
+
],
|
|
37
|
+
labelIdle: '', // Left empty because we use our own UI
|
|
14
38
|
credits: false,
|
|
15
|
-
|
|
16
|
-
maxFileSize: '10MB',
|
|
17
|
-
stylePanelLayout: null,
|
|
18
|
-
itemInsertLocation: 'after',
|
|
19
|
-
instantUpload: false,
|
|
39
|
+
storeAsFile: true, // Important for encode to work if used
|
|
20
40
|
});
|
|
21
41
|
|
|
22
|
-
|
|
42
|
+
// Expose globally so chat_main.js can access (getFiles, removeFiles)
|
|
43
|
+
window.filePond = filePond;
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
// 3. DOM references for the new custom UI
|
|
47
|
+
const dropzone = document.getElementById('chat-dropzone');
|
|
48
|
+
const fileListContainer = document.getElementById('inline-file-list');
|
|
49
|
+
const fileCounter = document.getElementById('file-counter');
|
|
50
|
+
const paperclipBtn = document.getElementById('paperclip-button');
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
// 4. Rendering Functions
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the Bootstrap icon class based on filename or file type.
|
|
57
|
+
*/
|
|
58
|
+
function getFileIconClass(filename, type) {
|
|
59
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
60
|
+
|
|
61
|
+
// Images
|
|
62
|
+
if (type.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) {
|
|
63
|
+
return 'bi-file-earmark-image';
|
|
64
|
+
}
|
|
65
|
+
// PDF
|
|
66
|
+
if (type === 'application/pdf' || ext === 'pdf') {
|
|
67
|
+
return 'bi-file-earmark-pdf';
|
|
68
|
+
}
|
|
69
|
+
// Excel / Spreadsheets
|
|
70
|
+
if (['xls', 'xlsx', 'csv'].includes(ext) || type.includes('spreadsheet') || type.includes('excel')) {
|
|
71
|
+
return 'bi-file-earmark-excel';
|
|
72
|
+
}
|
|
73
|
+
// Word / Documents
|
|
74
|
+
if (['doc', 'docx'].includes(ext) || type.includes('word') || type.includes('document')) {
|
|
75
|
+
return 'bi-file-earmark-word';
|
|
76
|
+
}
|
|
77
|
+
// Text / Code
|
|
78
|
+
if (['txt', 'md', 'json', 'py', 'js', 'html', 'css'].includes(ext)) {
|
|
79
|
+
return 'bi-file-earmark-text';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Default icon
|
|
83
|
+
return 'bi-file-earmark';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Rebuilds the visual file list below the input.
|
|
88
|
+
*/
|
|
89
|
+
function renderFileList() {
|
|
90
|
+
if (!fileListContainer) return;
|
|
23
91
|
|
|
24
|
-
// Función para actualizar la visibilidad del icono "ver archivos"
|
|
25
|
-
function updateFileIconsVisibility() {
|
|
26
92
|
const files = filePond.getFiles();
|
|
93
|
+
fileListContainer.innerHTML = ''; // Clear current list
|
|
94
|
+
|
|
27
95
|
if (files.length > 0) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
96
|
+
fileListContainer.style.display = 'block';
|
|
97
|
+
|
|
98
|
+
// Update counter
|
|
99
|
+
if (fileCounter) {
|
|
100
|
+
fileCounter.textContent = `${files.length}/${filePond.maxFiles || 5}`;
|
|
101
|
+
fileCounter.style.display = 'inline-block';
|
|
33
102
|
}
|
|
103
|
+
|
|
104
|
+
files.forEach(fileItem => {
|
|
105
|
+
const file = fileItem.file;
|
|
106
|
+
const iconClass = getFileIconClass(file.name, file.type || '');
|
|
107
|
+
|
|
108
|
+
// Create file row
|
|
109
|
+
const row = document.createElement('div');
|
|
110
|
+
row.className = 'file-list-item';
|
|
111
|
+
|
|
112
|
+
row.innerHTML = `
|
|
113
|
+
<i class="bi ${iconClass} file-icon"></i>
|
|
114
|
+
<span class="file-name" title="${file.name}">${file.name}</span>
|
|
115
|
+
<i class="bi bi-x-circle-fill file-remove" role="button" aria-label="Remove file"></i>
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
// Click event on the remove button of the row
|
|
119
|
+
const removeBtn = row.querySelector('.file-remove');
|
|
120
|
+
removeBtn.addEventListener('click', (e) => {
|
|
121
|
+
e.stopPropagation(); // Prevent click propagation if nested in a clickable area
|
|
122
|
+
filePond.removeFile(fileItem.id);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
fileListContainer.appendChild(row);
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
// Hide list and counter if no files
|
|
129
|
+
fileListContainer.style.display = 'none';
|
|
130
|
+
if (fileCounter) fileCounter.style.display = 'none';
|
|
34
131
|
}
|
|
35
132
|
}
|
|
36
133
|
|
|
37
|
-
// Función para poblar el modal con los archivos y botones de eliminar
|
|
38
|
-
function populateFilesModal() {
|
|
39
|
-
uploadedFilesList.empty(); // Limpiar lista anterior
|
|
40
|
-
const files = filePond.getFiles();
|
|
41
134
|
|
|
42
|
-
|
|
43
|
-
uploadedFilesList.append('<li class="list-group-item">No hay archivos adjuntos.</li>');
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
135
|
+
// 5. Interaction Event Management
|
|
46
136
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
137
|
+
// -- Custom Dropzone Robust Handling --
|
|
138
|
+
if (dropzone) {
|
|
139
|
+
// Prevent default browser behavior for drag events
|
|
140
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
141
|
+
dropzone.addEventListener(eventName, (e) => {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
e.stopPropagation();
|
|
144
|
+
}, false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Highlight dropzone on drag enter/over
|
|
148
|
+
['dragenter', 'dragover'].forEach(eventName => {
|
|
149
|
+
dropzone.addEventListener(eventName, () => {
|
|
150
|
+
dropzone.classList.add('drag-over');
|
|
151
|
+
}, false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Remove highlight on drag leave or drop
|
|
155
|
+
// Note: checking relatedTarget prevents flickering when dragging over child elements
|
|
156
|
+
dropzone.addEventListener('dragleave', (e) => {
|
|
157
|
+
if (!dropzone.contains(e.relatedTarget)) {
|
|
158
|
+
dropzone.classList.remove('drag-over');
|
|
159
|
+
}
|
|
160
|
+
}, false);
|
|
161
|
+
|
|
162
|
+
dropzone.addEventListener('drop', (e) => {
|
|
163
|
+
dropzone.classList.remove('drag-over');
|
|
164
|
+
|
|
165
|
+
// Pass dropped files to FilePond
|
|
166
|
+
// We convert FileList to Array to ensure compatibility
|
|
167
|
+
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
|
|
168
|
+
filePond.addFiles(Array.from(e.dataTransfer.files));
|
|
169
|
+
}
|
|
170
|
+
}, false);
|
|
171
|
+
|
|
172
|
+
// Click on zone opens native selector
|
|
173
|
+
dropzone.addEventListener('click', function() {
|
|
174
|
+
filePond.browse();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// -- "Clip" Button (Legacy design compatibility) --
|
|
179
|
+
if (paperclipBtn) {
|
|
180
|
+
paperclipBtn.addEventListener('click', function() {
|
|
181
|
+
filePond.browse();
|
|
57
182
|
});
|
|
58
183
|
}
|
|
59
184
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
185
|
+
|
|
186
|
+
// 6. FilePond Hooks for Reactivity
|
|
187
|
+
|
|
188
|
+
// On file add (even if validation error, FilePond manages array)
|
|
189
|
+
filePond.on('addfile', (error, file) => {
|
|
190
|
+
if (error) {
|
|
191
|
+
console.error('FilePond Error:', error);
|
|
192
|
+
// Optional: Show error toast if file is invalid (e.g., too large)
|
|
193
|
+
return;
|
|
66
194
|
}
|
|
195
|
+
renderFileList();
|
|
67
196
|
});
|
|
68
197
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
populateFilesModal();
|
|
73
|
-
uploadedFilesModal.modal('show');
|
|
74
|
-
});
|
|
75
|
-
uploadedFilesList.on('click', '.remove-file-btn', function () {
|
|
76
|
-
const fileIdToRemove = $(this).data('file-id');
|
|
77
|
-
if (fileIdToRemove) {
|
|
78
|
-
window.filePond.removeFile(fileIdToRemove);
|
|
79
|
-
}
|
|
198
|
+
// On file remove
|
|
199
|
+
filePond.on('removefile', (error, file) => {
|
|
200
|
+
renderFileList();
|
|
80
201
|
});
|
|
81
202
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
203
|
+
// Event for general error (e.g., type not allowed on drop)
|
|
204
|
+
filePond.on('warning', (error) => {
|
|
205
|
+
console.warn('FilePond Warning:', error);
|
|
206
|
+
});
|
|
85
207
|
|
|
208
|
+
// Initialization: Initial render in case browser cached state
|
|
209
|
+
renderFileList();
|
|
210
|
+
});
|
iatoolkit/static/js/chat_main.js
CHANGED
|
@@ -58,7 +58,6 @@ $(document).ready(function () {
|
|
|
58
58
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
|
|
62
61
|
/**
|
|
63
62
|
* Main function to handle sending a chat message.
|
|
64
63
|
*/
|
|
@@ -93,16 +92,16 @@ const handleChatMessage = async function () {
|
|
|
93
92
|
if (paramsString) { displayMessage += `: ${paramsString}`; }
|
|
94
93
|
}
|
|
95
94
|
|
|
96
|
-
// Simplificado: Si no hay mensaje, el 'finally' se encargará de limpiar.
|
|
97
95
|
if (!displayMessage) {
|
|
98
96
|
return;
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
const files = window.filePond.getFiles();
|
|
100
|
+
|
|
101
|
+
displayUserMessage(displayMessage, isEditable, question, files);
|
|
102
102
|
showSpinner();
|
|
103
103
|
resetAllInputs();
|
|
104
104
|
|
|
105
|
-
const files = window.filePond.getFiles();
|
|
106
105
|
const filesBase64 = await Promise.all(files.map(fileItem => toBase64(fileItem.file)));
|
|
107
106
|
|
|
108
107
|
const data = {
|
|
@@ -112,59 +111,18 @@ const handleChatMessage = async function () {
|
|
|
112
111
|
files: filesBase64.map(f => ({ filename: f.name, content: f.base64 })),
|
|
113
112
|
user_identifier: window.user_identifier,
|
|
114
113
|
model: (window.currentLlmModel || window.defaultLlmModel || '')
|
|
115
|
-
|
|
116
114
|
};
|
|
117
115
|
|
|
118
116
|
const responseData = await callToolkit("/api/llm_query", data, "POST");
|
|
119
|
-
if (responseData && responseData.answer) {
|
|
120
|
-
// CAMBIO: contenedor principal para la respuesta del bot
|
|
121
|
-
const botMessageContainer = $('<div>').addClass('bot-message-container');
|
|
122
|
-
|
|
123
|
-
// 1. Si hay reasoning_content, agregar el acordeón colapsable
|
|
124
|
-
if (responseData.reasoning_content) {
|
|
125
|
-
const uniqueId = 'reasoning-' + Date.now(); // ID único para el collapse
|
|
126
|
-
|
|
127
|
-
const reasoningBlock = $(`
|
|
128
|
-
<div class="reasoning-block">
|
|
129
|
-
<button class="reasoning-toggle btn btn-sm btn-link text-decoration-none p-0"
|
|
130
|
-
type="button"
|
|
131
|
-
data-bs-toggle="collapse"
|
|
132
|
-
data-bs-target="#${uniqueId}"
|
|
133
|
-
aria-expanded="false"
|
|
134
|
-
aria-controls="${uniqueId}">
|
|
135
|
-
<i class="bi bi-lightbulb me-1"></i> ${t_js('show_reasoning')}
|
|
136
|
-
</button>
|
|
137
|
-
|
|
138
|
-
<div class="collapse mt-2" id="${uniqueId}">
|
|
139
|
-
<div class="reasoning-card">
|
|
140
|
-
${responseData.reasoning_content}
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
`);
|
|
145
|
-
botMessageContainer.append(reasoningBlock);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 2. Agregar la respuesta final
|
|
149
|
-
const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
|
|
150
|
-
botMessageContainer.append(answerSection);
|
|
151
117
|
|
|
152
|
-
|
|
153
|
-
|
|
118
|
+
// Delegamos el procesamiento de la respuesta a la nueva función
|
|
119
|
+
processBotResponse(responseData);
|
|
154
120
|
|
|
155
|
-
}
|
|
156
121
|
} catch (error) {
|
|
157
122
|
if (error.name === 'AbortError') {
|
|
158
|
-
|
|
159
|
-
// Usando jQuery estándar para construir el elemento ---
|
|
160
|
-
const icon = $('<i>').addClass('bi bi-stop-circle me-2'); // Icono sin "fill" para un look más ligero
|
|
123
|
+
const icon = $('<i>').addClass('bi bi-stop-circle me-2');
|
|
161
124
|
const textSpan = $('<span>').text('La generación de la respuesta ha sido detenida.');
|
|
162
|
-
|
|
163
|
-
const abortMessage = $('<div>')
|
|
164
|
-
.addClass('system-message')
|
|
165
|
-
.append(icon)
|
|
166
|
-
.append(textSpan);
|
|
167
|
-
|
|
125
|
+
const abortMessage = $('<div>').addClass('system-message').append(icon).append(textSpan);
|
|
168
126
|
displayBotMessage(abortMessage);
|
|
169
127
|
} else {
|
|
170
128
|
console.error("Error in handleChatMessage:", error);
|
|
@@ -172,17 +130,78 @@ const handleChatMessage = async function () {
|
|
|
172
130
|
displayBotMessage(errorSection);
|
|
173
131
|
}
|
|
174
132
|
} finally {
|
|
175
|
-
// Este bloque se ejecuta siempre, garantizando que el estado se limpie.
|
|
176
133
|
isRequestInProgress = false;
|
|
177
134
|
hideSpinner();
|
|
178
135
|
toggleSendStopButtons(false);
|
|
179
136
|
updateSendButtonState();
|
|
180
137
|
if (window.filePond) {
|
|
181
|
-
|
|
138
|
+
window.filePond.removeFiles();
|
|
182
139
|
}
|
|
183
140
|
}
|
|
184
141
|
};
|
|
185
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Processes the response data from the LLM and displays it in the chat.
|
|
145
|
+
* Handles multimodal content: uses 'answer' for text (HTML) and 'content_parts' for images.
|
|
146
|
+
* @param {object} responseData - The JSON response from the server.
|
|
147
|
+
*/
|
|
148
|
+
function processBotResponse(responseData) {
|
|
149
|
+
if (!responseData || (!responseData.answer && !responseData.content_parts)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const botMessageContainer = $('<div>').addClass('bot-message-container');
|
|
154
|
+
|
|
155
|
+
// 1. Si hay reasoning_content, agregar el acordeón colapsable
|
|
156
|
+
if (responseData.reasoning_content) {
|
|
157
|
+
const uniqueId = 'reasoning-' + Date.now();
|
|
158
|
+
const reasoningBlock = $(`
|
|
159
|
+
<div class="reasoning-block">
|
|
160
|
+
<button class="reasoning-toggle btn btn-sm btn-link text-decoration-none p-0"
|
|
161
|
+
type="button" data-bs-toggle="collapse" data-bs-target="#${uniqueId}"
|
|
162
|
+
aria-expanded="false" aria-controls="${uniqueId}">
|
|
163
|
+
<i class="bi bi-lightbulb me-1"></i> ${t_js('show_reasoning')}
|
|
164
|
+
</button>
|
|
165
|
+
<div class="collapse mt-2" id="${uniqueId}">
|
|
166
|
+
<div class="reasoning-card">${responseData.reasoning_content}</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
`);
|
|
170
|
+
botMessageContainer.append(reasoningBlock);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Agregar la respuesta final
|
|
174
|
+
const answerSection = $('<div>').addClass('answer-section llm-output');
|
|
175
|
+
|
|
176
|
+
// A. Texto: Usamos 'answer' porque contiene el HTML procesado y limpio del backend.
|
|
177
|
+
// Evitamos usar content_parts[type=text] porque contiene el JSON crudo del LLM.
|
|
178
|
+
if (responseData.answer) {
|
|
179
|
+
answerSection.append(responseData.answer);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// B. Imágenes: Iteramos content_parts buscando SOLO imágenes para adjuntarlas.
|
|
183
|
+
if (responseData.content_parts && responseData.content_parts.length > 0) {
|
|
184
|
+
responseData.content_parts.forEach(part => {
|
|
185
|
+
if (part.type === 'image' && part.source && part.source.url) {
|
|
186
|
+
const imgContainer = $('<div>').addClass('image-part my-3 text-center');
|
|
187
|
+
const img = $('<img>')
|
|
188
|
+
.attr('src', part.source.url)
|
|
189
|
+
.addClass('img-fluid rounded shadow-sm border')
|
|
190
|
+
.css({'max-height': '400px', 'cursor': 'pointer'})
|
|
191
|
+
.on('click', () => window.open(part.source.url, '_blank'));
|
|
192
|
+
|
|
193
|
+
imgContainer.append(img);
|
|
194
|
+
answerSection.append(imgContainer);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
botMessageContainer.append(answerSection);
|
|
200
|
+
|
|
201
|
+
// 3. Mostrar el contenedor completo
|
|
202
|
+
displayBotMessage(botMessageContainer);
|
|
203
|
+
}
|
|
204
|
+
|
|
186
205
|
|
|
187
206
|
/**
|
|
188
207
|
* Resets all inputs to their initial state.
|
|
@@ -316,14 +335,48 @@ const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
|
|
|
316
335
|
* @param {string} message - The full message string to display.
|
|
317
336
|
* @param {boolean} isEditable - Determines if the edit icon should be shown.
|
|
318
337
|
* @param {string} originalQuestion - The original text to put back in the textarea for editing.
|
|
338
|
+
* @param {Array} [files] - Optional array of FilePond file items.
|
|
319
339
|
*/
|
|
320
|
-
const displayUserMessage = function(message, isEditable, originalQuestion) {
|
|
340
|
+
const displayUserMessage = function(message, isEditable, originalQuestion, files = []) {
|
|
321
341
|
const chatContainer = $('#chat-container');
|
|
322
342
|
const userMessage = $('<div>').addClass('message shadow-sm');
|
|
323
343
|
const messageText = $('<span>').text(message);
|
|
324
344
|
|
|
325
345
|
userMessage.append(messageText);
|
|
326
346
|
|
|
347
|
+
// Renderizar previsualizaciones de archivos si existen
|
|
348
|
+
if (files && files.length > 0) {
|
|
349
|
+
const attachmentsContainer = $('<div>').addClass('mt-2 d-flex flex-wrap gap-2 ms-3');
|
|
350
|
+
|
|
351
|
+
files.forEach(fileItem => {
|
|
352
|
+
const file = fileItem.file;
|
|
353
|
+
|
|
354
|
+
if (file.type && file.type.startsWith('image/')) {
|
|
355
|
+
// Previsualización de imagen usando URL temporal
|
|
356
|
+
const imgUrl = URL.createObjectURL(file);
|
|
357
|
+
const img = $('<img>')
|
|
358
|
+
.attr('src', imgUrl)
|
|
359
|
+
.addClass('rounded border')
|
|
360
|
+
.css({
|
|
361
|
+
'max-height': '80px',
|
|
362
|
+
'max-width': '120px',
|
|
363
|
+
'object-fit': 'cover',
|
|
364
|
+
'cursor': 'pointer'
|
|
365
|
+
})
|
|
366
|
+
.on('click', () => window.open(imgUrl, '_blank')); // Click para ver en grande
|
|
367
|
+
attachmentsContainer.append(img);
|
|
368
|
+
} else {
|
|
369
|
+
// Icono genérico para documentos
|
|
370
|
+
const badge = $('<span>')
|
|
371
|
+
.addClass('badge bg-light text-dark border p-2')
|
|
372
|
+
.html(`<i class="bi bi-file-earmark-text me-1"></i> ${file.name}`);
|
|
373
|
+
attachmentsContainer.append(badge);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
userMessage.append(attachmentsContainer);
|
|
378
|
+
}
|
|
379
|
+
|
|
327
380
|
if (isEditable) {
|
|
328
381
|
const editIcon = $('<i>').addClass('p-2 bi bi-pencil-fill edit-icon edit-pencil').attr('title', 'Edit query').on('click', function () {
|
|
329
382
|
$('#question').val(originalQuestion)
|