From 14a64778b8fe2475c612788673eaf8e66fbbd0d6 Mon Sep 17 00:00:00 2001 From: ronalds Date: Sun, 30 Nov 2025 23:23:43 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Nuevo=20Tipo=20de=20Pregunta:=20Asi?= =?UTF-8?q?stente=20IA=20(Chat)=20=F0=9F=A4=96=F0=9F=92=AC=20Frontend=20(v?= =?UTF-8?q?1.0.88)=20QuestionTypeEditor.jsx=20=E2=9C=85=20Nuevo=20tipo:=20?= =?UTF-8?q?ai=5Fassistant=20con=20icono=20=F0=9F=92=AC=20=E2=9C=85=20Confi?= =?UTF-8?q?guraci=C3=B3n=20completa:=20assistant=5Fprompt:=20Define=20rol?= =?UTF-8?q?=20y=20comportamiento=20del=20asistente=20context=5Fquestions:?= =?UTF-8?q?=20IDs=20de=20preguntas=20anteriores=20cuyas=20fotos=20usar=20(?= =?UTF-8?q?o=20todas)=20assistant=5Finstructions:=20Reglas=20espec=C3=ADfi?= =?UTF-8?q?cas=20de=20diagn=C3=B3stico=20max=5Fmessages:=20L=C3=ADmite=20d?= =?UTF-8?q?e=20mensajes=20en=20el=20chat=20response=5Flength:=20Corta/Medi?= =?UTF-8?q?a/Larga=20QuestionAnswerInput.jsx=20=E2=9C=85=20Mensaje=20infor?= =?UTF-8?q?mativo=20para=20tipo=20ai=5Fassistant=20=E2=9C=85=20Indica=20qu?= =?UTF-8?q?e=20el=20chat=20se=20abre=20con=20bot=C3=B3n=20separado=20App.j?= =?UTF-8?q?sx=20-=20Modal=20de=20Chat=20IA=20=E2=9C=85=20Modal=20full-scre?= =?UTF-8?q?en=20responsive=20con:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Header con gradiente purple/blue Área de mensajes con scroll Input de texto + botón enviar Soporte Enter para enviar Indicador de "pensando..." Timestamps en mensajes Confianza de la IA Límite de mensajes ✅ Botón "💬 Consultar Asistente IA" al lado de "Cargar Documentos" ✅ Contador de mensajes en el botón si ya hay historial ✅ Historial guardado en answers[questionId].chatHistory ✅ Auto-marca como completada cuando se abre el chat Backend (v1.0.85) Endpoint /api/ai/chat-assistant ✅ Recibe: Mensaje del usuario Historial del chat Fotos de preguntas anteriores con sus análisis Configuración del asistente Info del vehículo ✅ Contexto inteligente: Información del vehículo (marca, modelo, km, placa) Análisis previos de fotos (hasta 10 fotos más relevantes) Instrucciones personalizadas por pregunta Historial conversacional (últimos 10 mensajes) ✅ Soporte multi-proveedor: OpenAI (GPT-4, GPT-3.5) Google Gemini ✅ Respuestas adaptables: Longitud configurable (200/400/800 tokens) Temperature 0.7 para balance creatividad/precisión Cómo Funciona Configuración (Admin): - Crear pregunta tipo "Asistente IA (Chat)" - Definir prompt: "Eres experto en sistemas de frenos..." - Seleccionar preguntas anteriores: "5,8,12" (fotos de frenos) - Instrucciones: "Si pastillas < 3mm → cambio inmediato" - Longitud: Media - Max mensajes: 20 2 Durante Inspección (Mecánico): - Llega a pregunta tipo AI Assistant - Ve botón "💬 Consultar Asistente IA" - Clic → Abre chat full-screen - La IA YA vio las fotos de preguntas 5, 8, 12 - Mecánico pregunta: "¿Las pastillas están bien?" - IA responde basado en fotos previas + contexto vehículo - Conversación natural con historial - Al cerrar → se guarda automáticamente 3 Ejemplo de Conversación: Mecánico: "Vi desgaste en las pastillas, ¿qué recomiendas?" IA: "Basándome en las fotos que analicé: Pastilla delantera izquierda: ~2.5mm de espesor Pastilla delantera derecha: ~2.8mm ⚠️ CRÍTICO: Ambas pastillas están por debajo del mínimo de 3mm. Con 45,000 km, es normal este desgaste. RECOMENDACIÓN: - Cambio INMEDIATO de pastillas delanteras - Verificar estado de discos (posible rectificado) - Revisar líquido de frenos (última foto muestra nivel bajo) ¿Necesitas el código de pieza para este Toyota Corolla 2019?" Casos de Uso Diagnóstico de Frenos assistant_prompt: "Eres especialista en sistemas de frenos. Analiza desgaste, fugas, vibraciones." context_questions: "10,11,12,13" // Fotos de pastillas, discos, líquido assistant_prompt: "Experto en motores. Detecta fugas, ruidos anormales, consumo excesivo." context_questions: "5,6,7,8,9" // Motor, aceite, correa, filtros assistant_prompt: "Especialista en sistemas eléctricos y electrónicos." context_questions: "20,21,22" // Batería, luces, tablero instructions: "Siempre pedir código OBD2 si hay check engine" Ventajas ✅ Contextual: La IA ve fotos previas, no pregunta "¿puedes mostrarme?" ✅ Especializado: Un asistente POR tema (frenos, motor, eléctrico) ✅ Conversacional: El mecánico puede hacer follow-up questions ✅ Guiado: Instrucciones específicas por tipo de inspección ✅ Historial: No repite info, mantiene contexto de la conversación ✅ Móvil-friendly: Modal responsive, fácil de usar en celular --- backend/app/main.py | 171 +++++++++++++++- frontend/package.json | 2 +- frontend/public/service-worker.js | 2 +- frontend/src/App.jsx | 284 +++++++++++++++++++++++++++ frontend/src/QuestionAnswerInput.jsx | 19 ++ frontend/src/QuestionTypeEditor.jsx | 118 ++++++++++- 6 files changed, 592 insertions(+), 4 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index f80731b..65b0c2d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -204,7 +204,7 @@ def send_completed_inspection_to_n8n(inspection, db): # No lanzamos excepción para no interrumpir el flujo normal -BACKEND_VERSION = "1.0.84" +BACKEND_VERSION = "1.0.85" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -2849,6 +2849,175 @@ Responde en formato JSON: } +@app.post("/api/ai/chat-assistant") +async def chat_with_ai_assistant( + request: dict, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + """ + Chat conversacional con IA usando contexto de fotos anteriores + El asistente tiene acceso a fotos de preguntas previas para dar mejor contexto + """ + print("\n" + "="*80) + print("🤖 AI CHAT ASSISTANT") + print("="*80) + + question_id = request.get('question_id') + inspection_id = request.get('inspection_id') + user_message = request.get('user_message') + chat_history = request.get('chat_history', []) + context_photos = request.get('context_photos', []) + assistant_prompt = request.get('assistant_prompt', '') + assistant_instructions = request.get('assistant_instructions', '') + response_length = request.get('response_length', 'medium') + vehicle_info = request.get('vehicle_info', {}) + + print(f"📋 Question ID: {question_id}") + print(f"🚗 Inspection ID: {inspection_id}") + print(f"💬 User message: {user_message}") + print(f"📸 Context photos: {len(context_photos)} fotos") + print(f"💭 Chat history: {len(chat_history)} mensajes previos") + + # Obtener configuración de IA + ai_config = db.query(models.AIConfiguration).filter( + models.AIConfiguration.is_active == True + ).first() + + if not ai_config: + return { + "success": False, + "response": "No hay configuración de IA activa. Por favor configura en Settings.", + "confidence": 0 + } + + try: + # Construir el contexto del vehículo + vehicle_context = f""" +INFORMACIÓN DEL VEHÍCULO: +- Marca: {vehicle_info.get('brand', 'N/A')} +- Modelo: {vehicle_info.get('model', 'N/A')} +- Placa: {vehicle_info.get('plate', 'N/A')} +- Kilometraje: {vehicle_info.get('km', 'N/A')} km +""" + + # Construir el contexto de las fotos anteriores + photos_context = "" + if context_photos: + photos_context = f"\n\nFOTOS ANALIZADAS PREVIAMENTE ({len(context_photos)} imágenes):\n" + for idx, photo in enumerate(context_photos[:10], 1): # Limitar a 10 fotos + ai_analysis = photo.get('aiAnalysis', []) + if ai_analysis and len(ai_analysis) > 0: + analysis_text = ai_analysis[0].get('analysis', {}) + obs = analysis_text.get('observations', 'Sin análisis') + status = analysis_text.get('status', 'unknown') + photos_context += f"\n{idx}. Pregunta ID {photo.get('questionId')}: Status={status}\n Observaciones: {obs[:200]}...\n" + + # Definir la longitud de respuesta + max_tokens_map = { + 'short': 200, + 'medium': 400, + 'long': 800 + } + max_tokens = max_tokens_map.get(response_length, 400) + + # Construir el system prompt + base_prompt = assistant_prompt or "Eres un experto mecánico automotriz que ayuda a diagnosticar problemas." + + system_prompt = f"""{base_prompt} + +{vehicle_context} + +{photos_context} + +INSTRUCCIONES ADICIONALES: +{assistant_instructions if assistant_instructions else "Sé técnico, claro y directo en tus respuestas."} + +FORMATO DE RESPUESTA: +- Sé {response_length} en tus respuestas +- Usa lenguaje técnico pero comprensible +- Si ves algo preocupante en las fotos analizadas, menciónalo +- Proporciona recomendaciones específicas cuando sea relevante +- Si no tienes suficiente información, pide más detalles +""" + + # Construir el historial de mensajes para la IA + messages = [{"role": "system", "content": system_prompt}] + + # Agregar historial previo (últimos 10 mensajes para no saturar) + for msg in chat_history[-10:]: + messages.append({ + "role": msg.get('role'), + "content": msg.get('content') + }) + + # Agregar el mensaje actual del usuario + messages.append({ + "role": "user", + "content": user_message + }) + + print(f"🔧 Enviando a {ai_config.provider} con {len(messages)} mensajes") + + # Llamar a la IA según el proveedor + if ai_config.provider == 'openai': + from openai import OpenAI + client = OpenAI(api_key=ai_config.api_key) + + response = client.chat.completions.create( + model=ai_config.model or "gpt-4", + messages=messages, + max_tokens=max_tokens, + temperature=0.7 + ) + + ai_response = response.choices[0].message.content + confidence = 0.85 # OpenAI no devuelve confidence directo + + elif ai_config.provider == 'gemini': + import google.generativeai as genai + genai.configure(api_key=ai_config.api_key) + + model = genai.GenerativeModel(ai_config.model or 'gemini-pro') + + # Gemini maneja el chat diferente + # Convertir mensajes al formato de Gemini + chat_content = "" + for msg in messages[1:]: # Skip system message + role_label = "Usuario" if msg['role'] == 'user' else "Asistente" + chat_content += f"\n{role_label}: {msg['content']}\n" + + full_prompt = f"{system_prompt}\n\nCONVERSACIÓN:\n{chat_content}\n\nAsistente:" + + response = model.generate_content(full_prompt) + ai_response = response.text + confidence = 0.80 + + else: + raise ValueError(f"Proveedor no soportado: {ai_config.provider}") + + print(f"✅ Respuesta generada: {len(ai_response)} caracteres") + + return { + "success": True, + "response": ai_response, + "confidence": confidence, + "provider": ai_config.provider, + "model": ai_config.model + } + + except Exception as e: + print(f"❌ Error en chat IA: {e}") + import traceback + traceback.print_exc() + + return { + "success": False, + "response": f"Error al comunicarse con el asistente: {str(e)}", + "confidence": 0 + } + + # ============= REPORTS ============= @app.get("/api/reports/dashboard", response_model=schemas.DashboardData) def get_dashboard_data( diff --git a/frontend/package.json b/frontend/package.json index d562a47..ab5d65b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "checklist-frontend", "private": true, - "version": "1.0.87", + "version": "1.0.88", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js index f1653f2..5c17b67 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.0.87'; +const CACHE_NAME = 'ayutec-v1.0.88'; const urlsToCache = [ '/', '/index.html' diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8878e81..13fb162 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4054,6 +4054,11 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) const [aiAnalyzing, setAiAnalyzing] = useState(false) + // AI Assistant Chat + const [showAIChat, setShowAIChat] = useState(false) + const [aiChatMessages, setAiChatMessages] = useState([]) + const [aiChatLoading, setAiChatLoading] = useState(false) + // Signature canvas const mechanicSigRef = useRef(null) @@ -5031,6 +5036,29 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl )} )} + + {/* Botón para abrir chat IA (si es tipo ai_assistant) */} + {currentQuestion.options?.type === 'ai_assistant' && ( + + )} )} @@ -5171,6 +5199,262 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl )} + + {/* Modal de Chat IA Asistente */} + {showAIChat && currentQuestion && ( + { + setShowAIChat(false) + // Guardar historial del chat en la respuesta + setAnswers(prev => ({ + ...prev, + [currentQuestion.id]: { + ...(prev[currentQuestion.id] || {}), + chatHistory: aiChatMessages, + value: 'chat_completed' // Marcar como respondida + } + })) + saveAnswer(currentQuestion.id) + }} + /> + )} + + ) +} + +// Componente Modal de Chat IA Asistente +function AIAssistantChatModal({ question, inspection, allAnswers, messages, setMessages, loading, setLoading, onClose }) { + const [inputMessage, setInputMessage] = useState('') + const chatEndRef = useRef(null) + const config = question.options || {} + + // Auto-scroll al final + useEffect(() => { + chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages]) + + // Enviar mensaje al asistente + const sendMessage = async () => { + if (!inputMessage.trim() || loading) return + + const userMessage = { role: 'user', content: inputMessage, timestamp: new Date().toISOString() } + setMessages(prev => [...prev, userMessage]) + setInputMessage('') + setLoading(true) + + try { + const token = localStorage.getItem('token') + const API_URL = import.meta.env.VITE_API_URL || '' + + // Recopilar fotos de preguntas anteriores según configuración + const contextPhotos = [] + const contextQuestionIds = config.context_questions + ? config.context_questions.split(',').map(id => parseInt(id.trim())) + : Object.keys(allAnswers).map(id => parseInt(id)) + + // Filtrar solo preguntas anteriores a la actual + const previousQuestionIds = contextQuestionIds.filter(id => id < question.id) + + previousQuestionIds.forEach(qId => { + const answer = allAnswers[qId] + if (answer?.photos && answer.photos.length > 0) { + answer.photos.forEach(photoUrl => { + contextPhotos.push({ + questionId: qId, + url: photoUrl, + aiAnalysis: answer.aiAnalysis + }) + }) + } + }) + + // Preparar el payload + const payload = { + question_id: question.id, + inspection_id: inspection.id, + user_message: inputMessage, + chat_history: messages, + context_photos: contextPhotos, + assistant_prompt: config.assistant_prompt || '', + assistant_instructions: config.assistant_instructions || '', + response_length: config.response_length || 'medium', + vehicle_info: { + brand: inspection.vehicle_brand, + model: inspection.vehicle_model, + plate: inspection.vehicle_plate, + km: inspection.vehicle_km + } + } + + console.log('📤 Enviando a chat IA:', payload) + + const response = await fetch(`${API_URL}/api/ai/chat-assistant`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + throw new Error('Error en respuesta del servidor') + } + + const data = await response.json() + console.log('📥 Respuesta de IA:', data) + + const assistantMessage = { + role: 'assistant', + content: data.response, + timestamp: new Date().toISOString(), + confidence: data.confidence + } + + setMessages(prev => [...prev, assistantMessage]) + + } catch (error) { + console.error('Error al enviar mensaje:', error) + const errorMessage = { + role: 'assistant', + content: '❌ Error al comunicarse con el asistente. Por favor intenta nuevamente.', + timestamp: new Date().toISOString(), + isError: true + } + setMessages(prev => [...prev, errorMessage]) + } finally { + setLoading(false) + } + } + + return ( +
+
+ {/* Header */} +
+
+
+
+ 💬 +

Asistente IA

+
+

+ {question.text} +

+
+ + {Object.values(allAnswers).filter(a => a.photos?.length > 0).length} preguntas con fotos + + + {messages.length} mensajes + +
+
+ +
+
+ + {/* Chat Messages */} +
+ {messages.length === 0 && ( +
+
🤖
+

+ ¡Hola! Soy tu asistente técnico. +

+

+ He analizado las fotos anteriores. ¿En qué puedo ayudarte? +

+
+ )} + + {messages.map((msg, idx) => ( +
+
+
+ {msg.content} +
+
+ {new Date(msg.timestamp).toLocaleTimeString('es-ES', { + hour: '2-digit', + minute: '2-digit' + })} + {msg.confidence && ( + • Confianza: {Math.round(msg.confidence * 100)}% + )} +
+
+
+ ))} + + {loading && ( +
+
+
+
+ El asistente está pensando... +
+
+
+ )} + +
+
+ + {/* Input */} +
+
+ setInputMessage(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !e.shiftKey && sendMessage()} + placeholder="Escribe tu pregunta..." + disabled={loading} + className="flex-1 px-3 sm:px-4 py-2 sm:py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm sm:text-base disabled:bg-gray-100" + /> + +
+ {config.max_messages && messages.length >= config.max_messages && ( +

+ ⚠️ Has alcanzado el límite de {config.max_messages} mensajes +

+ )} +
+
) } diff --git a/frontend/src/QuestionAnswerInput.jsx b/frontend/src/QuestionAnswerInput.jsx index 36fdce0..0b65dab 100644 --- a/frontend/src/QuestionAnswerInput.jsx +++ b/frontend/src/QuestionAnswerInput.jsx @@ -292,6 +292,25 @@ export function QuestionAnswerInput({ question, value, onChange, onSave }) { ) } + // AI_ASSISTANT (Chat con IA) + if (questionType === 'ai_assistant') { + return ( +
+
+ 💬 +

Asistente IA Disponible

+
+

+ Haz clic en el botón "💬 Consultar Asistente" debajo para abrir el chat con IA. + El asistente ha analizado las fotos anteriores y está listo para ayudarte. +

+
+ ℹ️ No requiere respuesta manual - el chat se guarda automáticamente +
+
+ ) + } + // Fallback para tipos desconocidos return (
diff --git a/frontend/src/QuestionTypeEditor.jsx b/frontend/src/QuestionTypeEditor.jsx index f9ae8a2..a4370ad 100644 --- a/frontend/src/QuestionTypeEditor.jsx +++ b/frontend/src/QuestionTypeEditor.jsx @@ -23,7 +23,8 @@ const QUESTION_TYPES = [ { value: 'number', label: '🔢 Número', icon: '#️⃣' }, { value: 'date', label: '📅 Fecha', icon: '📆' }, { value: 'time', label: '🕐 Hora', icon: '⏰' }, - { value: 'photo_only', label: '📸 Solo Fotografía', icon: '📷' } + { value: 'photo_only', label: '📸 Solo Fotografía', icon: '📷' }, + { value: 'ai_assistant', label: '🤖 Asistente IA (Chat)', icon: '💬' } ] const STATUS_OPTIONS = [ @@ -512,6 +513,121 @@ export function QuestionTypeEditor({ value, onChange, maxPoints = 1 }) {
)} + + {/* AI ASSISTANT (CHAT) */} + {config.type === 'ai_assistant' && ( +
+
+
+ 💬 +
+

Asistente IA Conversacional

+

+ El mecánico podrá chatear con IA usando fotos de preguntas anteriores como contexto. + Configura qué preguntas anteriores usar y el comportamiento del asistente. +

+
+
+
+ + {/* Prompt del asistente */} +
+ +