iatoolkit 1.9.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 +1 -1
- 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 +47 -2
- iatoolkit/locales/es.yaml +45 -1
- iatoolkit/repositories/llm_query_repo.py +44 -33
- iatoolkit/services/company_context_service.py +294 -133
- iatoolkit/services/dispatcher_service.py +1 -1
- iatoolkit/services/knowledge_base_service.py +26 -4
- iatoolkit/services/llm_client_service.py +58 -2
- iatoolkit/services/prompt_service.py +236 -330
- 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 +43 -3
- iatoolkit/views/chat_view.py +1 -1
- iatoolkit/views/login_view.py +1 -1
- iatoolkit/views/prompt_api_view.py +1 -1
- {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/METADATA +1 -1
- {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/RECORD +38 -37
- {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/WHEEL +0 -0
- {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/top_level.txt +0 -0
|
@@ -99,7 +99,7 @@ class QueryService:
|
|
|
99
99
|
final_client_data.update(client_data)
|
|
100
100
|
|
|
101
101
|
# Load attached files into the context
|
|
102
|
-
files_context = self.load_files_for_context(files)
|
|
102
|
+
files_context, images = self.load_files_for_context(files)
|
|
103
103
|
|
|
104
104
|
# Initialize prompt_content. It will be an empty string for direct questions.
|
|
105
105
|
main_prompt = ""
|
|
@@ -128,7 +128,7 @@ class QueryService:
|
|
|
128
128
|
else:
|
|
129
129
|
user_turn_prompt += f'\n### Contexto Adicional: El usuario ha aportado este contexto puede ayudar: {effective_question}'
|
|
130
130
|
|
|
131
|
-
return user_turn_prompt, effective_question
|
|
131
|
+
return user_turn_prompt, effective_question, images
|
|
132
132
|
|
|
133
133
|
def _ensure_valid_history(self, company,
|
|
134
134
|
user_identifier: str,
|
|
@@ -356,7 +356,7 @@ class QueryService:
|
|
|
356
356
|
effective_model = self._resolve_model(company_short_name, model)
|
|
357
357
|
|
|
358
358
|
# --- Build User-Facing Prompt ---
|
|
359
|
-
user_turn_prompt, effective_question = self._build_user_facing_prompt(
|
|
359
|
+
user_turn_prompt, effective_question, images = self._build_user_facing_prompt(
|
|
360
360
|
company=company,
|
|
361
361
|
user_identifier=user_identifier,
|
|
362
362
|
client_data=client_data,
|
|
@@ -397,7 +397,8 @@ class QueryService:
|
|
|
397
397
|
question=effective_question,
|
|
398
398
|
context=user_turn_prompt,
|
|
399
399
|
tools=tools,
|
|
400
|
-
text=output_schema
|
|
400
|
+
text=output_schema,
|
|
401
|
+
images=images,
|
|
401
402
|
)
|
|
402
403
|
|
|
403
404
|
if not response.get('valid_response'):
|
|
@@ -421,24 +422,23 @@ class QueryService:
|
|
|
421
422
|
return "unknown"
|
|
422
423
|
|
|
423
424
|
|
|
424
|
-
def load_files_for_context(self, files: list) -> str:
|
|
425
|
+
def load_files_for_context(self, files: list) -> tuple[str, list]:
|
|
425
426
|
"""
|
|
426
|
-
Processes a list of attached files
|
|
427
|
-
|
|
427
|
+
Processes a list of attached files.
|
|
428
|
+
Decodes text documents into context string and separates images for multimodal processing.
|
|
428
429
|
"""
|
|
429
430
|
if not files:
|
|
430
|
-
return ''
|
|
431
|
+
return '', []
|
|
432
|
+
|
|
433
|
+
context_parts = []
|
|
434
|
+
images = []
|
|
435
|
+
text_files_count = 0
|
|
431
436
|
|
|
432
|
-
context = f"""
|
|
433
|
-
A continuación encontraras una lista de documentos adjuntos
|
|
434
|
-
enviados por el usuario que hace la pregunta,
|
|
435
|
-
en total son: {len(files)} documentos adjuntos
|
|
436
|
-
"""
|
|
437
437
|
for document in files:
|
|
438
438
|
# Support both 'file_id' and 'filename' for robustness
|
|
439
439
|
filename = document.get('file_id') or document.get('filename') or document.get('name')
|
|
440
440
|
if not filename:
|
|
441
|
-
|
|
441
|
+
context_parts.append("\n<error>Documento adjunto sin nombre ignorado.</error>\n")
|
|
442
442
|
continue
|
|
443
443
|
|
|
444
444
|
# Support both 'base64' and 'content' for robustness
|
|
@@ -446,7 +446,12 @@ class QueryService:
|
|
|
446
446
|
|
|
447
447
|
if not base64_content:
|
|
448
448
|
# Handles the case where a file is referenced but no content is provided
|
|
449
|
-
|
|
449
|
+
context_parts.append(f"\n<error>El archivo '{filename}' no fue encontrado y no pudo ser cargado.</error>\n")
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
# Detect if the file is an image
|
|
453
|
+
if self._is_image(filename):
|
|
454
|
+
images.append({'name': filename, 'base64': base64_content})
|
|
450
455
|
continue
|
|
451
456
|
|
|
452
457
|
try:
|
|
@@ -456,12 +461,26 @@ class QueryService:
|
|
|
456
461
|
|
|
457
462
|
file_content = base64.b64decode(base64_content)
|
|
458
463
|
document_text = self.document_service.file_to_txt(filename, file_content)
|
|
459
|
-
|
|
464
|
+
context_parts.append(f"\n<document name='{filename}'>\n{document_text}\n</document>\n")
|
|
465
|
+
text_files_count += 1
|
|
460
466
|
except Exception as e:
|
|
461
467
|
# Catches errors from b64decode or file_to_txt
|
|
462
468
|
logging.error(f"Failed to process file {filename}: {e}")
|
|
463
|
-
|
|
469
|
+
context_parts.append(f"\n<error>Error al procesar el archivo {filename}: {str(e)}</error>\n")
|
|
464
470
|
continue
|
|
465
471
|
|
|
466
|
-
|
|
472
|
+
context = ""
|
|
473
|
+
if text_files_count > 0:
|
|
474
|
+
context = f"""
|
|
475
|
+
A continuación encontraras una lista de documentos adjuntos
|
|
476
|
+
enviados por el usuario que hace la pregunta,
|
|
477
|
+
en total son: {text_files_count} documentos adjuntos
|
|
478
|
+
""" + "".join(context_parts)
|
|
479
|
+
elif context_parts:
|
|
480
|
+
# If only errors were collected
|
|
481
|
+
context = "".join(context_parts)
|
|
482
|
+
|
|
483
|
+
return context, images
|
|
467
484
|
|
|
485
|
+
def _is_image(self, filename: str) -> bool:
|
|
486
|
+
return filename.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif'))
|
|
@@ -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
|
+
});
|