diff --git a/backend/app/main.py b/backend/app/main.py index 437d9ff..ba5d731 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -278,7 +278,7 @@ def extract_pdf_text_smart(pdf_content: bytes, max_chars: int = None) -> dict: } -BACKEND_VERSION = "1.2.6" +BACKEND_VERSION = "1.2.7" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -3484,10 +3484,30 @@ async def chat_with_ai_assistant( print(f"❌ Error procesando PDF {file.filename}: {pdf_result.get('error', 'Unknown')}") file_info['error'] = pdf_result.get('error', 'Error desconocido') - # Si es imagen, convertir a base64 + # Si es imagen, convertir a base64 Y subir a S3 elif file_type.startswith('image/'): file_info['content_type'] = 'image' file_info['base64'] = base64.b64encode(file_content).decode('utf-8') + + # Subir imagen a S3 para que tenga URL permanente + try: + file_extension = file.filename.split('.')[-1] if '.' in file.filename else 'jpg' + unique_filename = f"chat_{inspection_id}_{uuid.uuid4()}.{file_extension}" + + s3_client.upload_fileobj( + BytesIO(file_content), + app_config.settings.MINIO_IMAGE_BUCKET, + unique_filename, + ExtraArgs={'ContentType': file_type} + ) + + # Generar URL + image_url = f"{app_config.settings.MINIO_ENDPOINT}/{app_config.settings.MINIO_IMAGE_BUCKET}/{unique_filename}" + file_info['url'] = image_url + print(f"🖼️ Imagen subida a S3: {unique_filename}") + except Exception as upload_error: + print(f"⚠️ Error subiendo imagen a S3: {upload_error}") + # Continuar sin URL, usar base64 print(f"🖼️ Imagen procesada: {file.filename}") attached_files_data.append(file_info) @@ -3712,7 +3732,14 @@ Longitud de respuesta: {response_length} "confidence": confidence, "provider": ai_config.provider, "model": ai_config.model_name, - "attached_files": [{'filename': f['filename'], 'type': f['type']} for f in attached_files_data] + "attached_files": [ + { + 'filename': f['filename'], + 'type': f['type'], + 'url': f.get('url') # URL de S3 si es imagen + } + for f in attached_files_data + ] } except Exception as e: diff --git a/frontend/package.json b/frontend/package.json index fb8034c..6e5d99f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "checklist-frontend", "private": true, - "version": "1.2.8", + "version": "1.3.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js index 5787191..5bd0ea7 100644 --- a/frontend/public/service-worker.js +++ b/frontend/public/service-worker.js @@ -1,6 +1,6 @@ // Service Worker para PWA con detección de actualizaciones // IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión -const CACHE_NAME = 'ayutec-v1.2.8'; +const CACHE_NAME = 'ayutec-v1.3.0'; const urlsToCache = [ '/', '/index.html' diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ea27ed8..7ffb3d7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5656,12 +5656,31 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM // NUEVO: Agregar respuestas de texto (incluyendo observations) if (answer?.value || answer?.observations) { - // Buscar la pregunta para obtener su texto + // Buscar la pregunta para obtener su texto y tipo const questionData = inspection.checklist.questions.find(q => q.id === qId) + + // Formatear respuesta según el tipo de pregunta + let formattedAnswer = answer.value || '' + if (questionData?.options?.type) { + const qType = questionData.options.type + + if (qType === 'boolean') { + formattedAnswer = answer.value === 'true' ? 'Sí' : answer.value === 'false' ? 'No' : formattedAnswer + } else if (qType === 'single_choice' && questionData.options.choices) { + // Buscar la opción seleccionada + const selectedChoice = questionData.options.choices.find(c => c.value === answer.value) + formattedAnswer = selectedChoice?.label || answer.value + } else if (qType === 'scale') { + formattedAnswer = `${answer.value} (escala ${questionData.options.min || 1}-${questionData.options.max || 10})` + } + // Para text, number, date, time, multiple_choice, photo_only: usar valor directo + } + contextAnswers.push({ questionId: qId, questionText: questionData?.text || `Pregunta ${qId}`, - answer: answer.value || '', + questionType: questionData?.options?.type || 'text', + answer: formattedAnswer, observations: answer.observations || '' }) } @@ -5694,11 +5713,17 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM const data = await response.json() console.log('📥 Respuesta de IA:', data) + // Crear mensaje del asistente con archivos adjuntos (usando URLs del servidor) const assistantMessage = { role: 'assistant', content: data.response, timestamp: new Date().toISOString(), - confidence: data.confidence + confidence: data.confidence, + attachedFiles: data.attached_files?.map(f => ({ + filename: f.filename, + type: f.type, + url: f.url // URL de S3, no blob + })) || [] } setMessages(prev => [...prev, assistantMessage]) @@ -5827,6 +5852,34 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM ))} )} + {/* Mostrar archivos del asistente (URLs de S3) */} + {msg.attachedFiles && msg.attachedFiles.length > 0 && ( +
- Ayutec v1.2.8 + Ayutec v1.3.0