diff --git a/backend/app/main.py b/backend/app/main.py index 9db0ced..506d997 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -276,7 +276,7 @@ def extract_pdf_text_smart(pdf_content: bytes, max_chars: int = None) -> dict: } -BACKEND_VERSION = "1.0.95" +BACKEND_VERSION = "1.0.96" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -1807,8 +1807,8 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str: elements.append(Spacer(1, 3*mm)) # Cuadro de métricas con diseño moderno - metric_label = ParagraphStyle('metric_label', parent=small_style, fontSize=9, textColor=colors.HexColor('#64748b'), alignment=TA_CENTER) - metric_value = ParagraphStyle('metric_value', parent=info_style, fontSize=16, fontName='Helvetica-Bold', alignment=TA_CENTER) + metric_label = ParagraphStyle('metric_label', parent=small_style, fontSize=10, textColor=colors.HexColor('#64748b'), alignment=TA_CENTER) + metric_value = ParagraphStyle('metric_value', parent=info_style, fontSize=18, fontName='Helvetica-Bold', alignment=TA_CENTER) metrics_data = [ [Paragraph("Puntuación", metric_label), Paragraph("Ítems Críticos", metric_label)], @@ -1818,13 +1818,13 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str: ] ] - score_table = Table(metrics_data, colWidths=[90*mm, 90*mm]) + score_table = Table(metrics_data, colWidths=[90*mm, 90*mm], rowHeights=[12*mm, 18*mm]) score_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8fafc')), ('BACKGROUND', (0, 1), (-1, -1), colors.white), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), - ('PADDING', (0, 0), (-1, -1), 12), + ('PADDING', (0, 0), (-1, -1), 16), ('BOX', (0, 0), (-1, -1), 2, score_color), ('LINEABOVE', (0, 1), (-1, 1), 1.5, score_color), ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')), @@ -2733,12 +2733,19 @@ async def analyze_image( # Obtener contexto de la pregunta si se proporciona question_obj = None + question_options = [] if question_id: question_obj = db.query(models.Question).filter(models.Question.id == question_id).first() print(f"📋 Pregunta encontrada:") print(f" - ID: {question_obj.id}") print(f" - Texto: {question_obj.text}") + print(f" - Tipo: {question_obj.options.get('type') if question_obj.options else 'N/A'}") print(f" - ai_prompt en DB: {question_obj.ai_prompt[:100] if question_obj.ai_prompt else 'NO TIENE'}") + + # Extraer opciones de respuesta si existen + if question_obj.options and 'options' in question_obj.options: + question_options = question_obj.options['options'] + print(f" - Opciones disponibles: {question_options}") # Si no se proporciona custom_prompt en el Form, usar el de la pregunta if not custom_prompt and question_obj and question_obj.ai_prompt: @@ -2774,6 +2781,11 @@ INFORMACIÓN DEL VEHÍCULO INSPECCIONADO: try: # Construir prompt dinámico basado en la pregunta específica if question_obj: + # Agregar información de opciones de respuesta al prompt + options_context = "" + if question_options: + options_context = f"\n\nOPCIONES DE RESPUESTA DISPONIBLES:\n{', '.join(question_options)}\n\nEn el campo 'expected_answer', indica cuál de estas opciones es la más apropiada según lo que observas en la imagen." + # Usar prompt personalizado si está disponible if custom_prompt: # Prompt personalizado - DIRECTO Y SIMPLE @@ -2782,13 +2794,14 @@ INFORMACIÓN DEL VEHÍCULO INSPECCIONADO: {vehicle_context} TAREA ESPECÍFICA: -{custom_prompt} +{custom_prompt}{options_context} Responde SOLO en formato JSON válido (sin markdown, sin ```json): {{ "status": "ok", "observations": "Describe lo que observas en la imagen en relación a la tarea solicitada", "recommendation": "Acción sugerida basada en lo observado", + "expected_answer": "La respuesta que debería seleccionar el mecánico según lo observado (si hay opciones disponibles)", "confidence": 0.85, "context_match": true }} @@ -2818,7 +2831,7 @@ IMPORTANTE: {vehicle_context} PREGUNTA ESPECÍFICA A RESPONDER: "{question_text}" -Sección: {section} +Sección: {section}{options_context} Analiza la imagen ÚNICAMENTE para responder esta pregunta específica. Sé directo y enfócate solo en lo que la pregunta solicita. @@ -2833,6 +2846,7 @@ Responde SOLO en formato JSON válido (sin markdown, sin ```json): "status": "ok", "observations": "Respuesta técnica específica a: {question_text}", "recommendation": "Acción técnica recomendada o mensaje si la foto no es apropiada", + "expected_answer": "La respuesta correcta que debería seleccionar según lo observado", "confidence": 0.85, "context_match": true }} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f48bbd4..2e34423 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5273,6 +5273,29 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl alert('⚠️ Debes presionar "Cargar Documentos" antes de continuar') return } + + // NUEVA VALIDACIÓN: Verificar coherencia entre respuesta del mecánico y análisis de IA + const answer = answers[currentQuestion.id] + if (answer?.aiAnalysis && answer.aiAnalysis.length > 0 && answer.value) { + const aiData = answer.aiAnalysis[0]?.analysis + if (aiData?.expected_answer && aiData.expected_answer.toLowerCase() !== answer.value.toLowerCase()) { + const confirmChange = window.confirm( + `⚠️ ADVERTENCIA DE COHERENCIA\n\n` + + `Tu respuesta: "${answer.value}"\n` + + `Análisis de IA sugiere: "${aiData.expected_answer}"\n\n` + + `La imagen muestra:\n${aiData.observations || 'Sin detalles'}\n\n` + + `¿Deseas cambiar tu respuesta antes de continuar?\n\n` + + `• Presiona CANCELAR para revisar y cambiar tu respuesta\n` + + `• Presiona ACEPTAR para continuar con tu respuesta actual` + ) + + if (!confirmChange) { + // El mecánico quiere cambiar su respuesta + return + } + } + } + saveAnswer(currentQuestion.id) goToQuestion(currentQuestionIndex + 1) }} @@ -5296,6 +5319,29 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl alert('⚠️ Debes presionar "Cargar Documentos" antes de continuar') return } + + // NUEVA VALIDACIÓN: Verificar coherencia entre respuesta del mecánico y análisis de IA + const answer = answers[currentQuestion.id] + if (answer?.aiAnalysis && answer.aiAnalysis.length > 0 && answer.value) { + const aiData = answer.aiAnalysis[0]?.analysis + if (aiData?.expected_answer && aiData.expected_answer.toLowerCase() !== answer.value.toLowerCase()) { + const confirmChange = window.confirm( + `⚠️ ADVERTENCIA DE COHERENCIA\n\n` + + `Tu respuesta: "${answer.value}"\n` + + `Análisis de IA sugiere: "${aiData.expected_answer}"\n\n` + + `La imagen muestra:\n${aiData.observations || 'Sin detalles'}\n\n` + + `¿Deseas cambiar tu respuesta antes de completar?\n\n` + + `• Presiona CANCELAR para revisar y cambiar tu respuesta\n` + + `• Presiona ACEPTAR para continuar con tu respuesta actual` + ) + + if (!confirmChange) { + // El mecánico quiere cambiar su respuesta + return + } + } + } + saveAnswer(currentQuestion.id) proceedToSignatures() }}