✅ Nuevo Tipo de Pregunta: Asistente IA (Chat) 🤖💬
Frontend (v1.0.88) QuestionTypeEditor.jsx ✅ Nuevo tipo: ai_assistant con icono 💬 ✅ Configuración completa: assistant_prompt: Define rol y comportamiento del asistente context_questions: IDs de preguntas anteriores cuyas fotos usar (o todas) assistant_instructions: Reglas específicas de diagnóstico max_messages: Límite de mensajes en el chat response_length: Corta/Media/Larga QuestionAnswerInput.jsx ✅ Mensaje informativo para tipo ai_assistant ✅ Indica que el chat se abre con botón separado App.jsx - Modal de Chat IA ✅ Modal full-screen responsive con: 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
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "checklist-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.87",
|
||||
"version": "1.0.88",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Botón para abrir chat IA (si es tipo ai_assistant) */}
|
||||
{currentQuestion.options?.type === 'ai_assistant' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAIChat(true)
|
||||
// Cargar historial si existe
|
||||
if (answers[currentQuestion.id]?.chatHistory) {
|
||||
setAiChatMessages(answers[currentQuestion.id].chatHistory)
|
||||
}
|
||||
}}
|
||||
className="w-full mt-3 px-4 py-3 bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-lg hover:from-purple-700 hover:to-blue-700 transition flex items-center justify-center gap-2 font-semibold shadow-lg"
|
||||
>
|
||||
<span>💬</span>
|
||||
<span>Consultar Asistente IA</span>
|
||||
{answers[currentQuestion.id]?.chatHistory?.length > 0 && (
|
||||
<span className="ml-1 px-2 py-0.5 bg-white/20 rounded-full text-xs">
|
||||
{answers[currentQuestion.id].chatHistory.length} mensajes
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5171,6 +5199,262 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal de Chat IA Asistente */}
|
||||
{showAIChat && currentQuestion && (
|
||||
<AIAssistantChatModal
|
||||
question={currentQuestion}
|
||||
inspection={{ id: inspectionId, ...vehicleData }}
|
||||
allAnswers={answers}
|
||||
messages={aiChatMessages}
|
||||
setMessages={setAiChatMessages}
|
||||
loading={aiChatLoading}
|
||||
setLoading={setAiChatLoading}
|
||||
onClose={() => {
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-[100] p-2 sm:p-4">
|
||||
<div className="bg-white rounded-xl w-full max-w-4xl max-h-[95vh] flex flex-col shadow-2xl">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-purple-600 to-blue-600 text-white p-4 sm:p-6 rounded-t-xl">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-3xl">💬</span>
|
||||
<h3 className="text-lg sm:text-xl font-bold">Asistente IA</h3>
|
||||
</div>
|
||||
<p className="text-sm sm:text-base text-purple-100 line-clamp-2">
|
||||
{question.text}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs">
|
||||
<span className="px-2 py-1 bg-white/20 rounded">
|
||||
{Object.values(allAnswers).filter(a => a.photos?.length > 0).length} preguntas con fotos
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-white/20 rounded">
|
||||
{messages.length} mensajes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/80 hover:text-white text-3xl flex-shrink-0"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 sm:p-6 space-y-4 bg-gray-50">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🤖</div>
|
||||
<p className="text-gray-600 text-sm sm:text-base mb-2">
|
||||
¡Hola! Soy tu asistente técnico.
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs sm:text-sm">
|
||||
He analizado las fotos anteriores. ¿En qué puedo ayudarte?
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((msg, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[85%] sm:max-w-[75%] rounded-lg p-3 sm:p-4 ${
|
||||
msg.role === 'user'
|
||||
? 'bg-blue-600 text-white'
|
||||
: msg.isError
|
||||
? 'bg-red-50 border border-red-200 text-red-800'
|
||||
: 'bg-white border border-gray-200 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm sm:text-base whitespace-pre-wrap break-words">
|
||||
{msg.content}
|
||||
</div>
|
||||
<div
|
||||
className={`text-xs mt-2 ${
|
||||
msg.role === 'user' ? 'text-blue-100' : 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{new Date(msg.timestamp).toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
{msg.confidence && (
|
||||
<span className="ml-2">• Confianza: {Math.round(msg.confidence * 100)}%</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{loading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<div className="animate-spin h-4 w-4 border-2 border-purple-600 border-t-transparent rounded-full"></div>
|
||||
<span className="text-sm">El asistente está pensando...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={chatEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="border-t p-3 sm:p-4 bg-white rounded-b-xl">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputMessage}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={sendMessage}
|
||||
disabled={!inputMessage.trim() || loading}
|
||||
className="px-4 sm:px-6 py-2 sm:py-3 bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-lg hover:from-purple-700 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition text-sm sm:text-base font-semibold"
|
||||
>
|
||||
Enviar
|
||||
</button>
|
||||
</div>
|
||||
{config.max_messages && messages.length >= config.max_messages && (
|
||||
<p className="text-xs text-amber-600 mt-2">
|
||||
⚠️ Has alcanzado el límite de {config.max_messages} mensajes
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -292,6 +292,25 @@ export function QuestionAnswerInput({ question, value, onChange, onSave }) {
|
||||
)
|
||||
}
|
||||
|
||||
// AI_ASSISTANT (Chat con IA)
|
||||
if (questionType === 'ai_assistant') {
|
||||
return (
|
||||
<div className="p-4 bg-gradient-to-r from-purple-50 to-blue-50 border-2 border-purple-200 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-2xl">💬</span>
|
||||
<h4 className="font-semibold text-purple-900">Asistente IA Disponible</h4>
|
||||
</div>
|
||||
<p className="text-sm text-purple-700 mb-2">
|
||||
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.
|
||||
</p>
|
||||
<div className="text-xs text-purple-600 bg-white/50 rounded px-2 py-1">
|
||||
ℹ️ No requiere respuesta manual - el chat se guarda automáticamente
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback para tipos desconocidos
|
||||
return (
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI ASSISTANT (CHAT) */}
|
||||
{config.type === 'ai_assistant' && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-3xl">💬</span>
|
||||
<div>
|
||||
<h4 className="font-semibold text-blue-900 mb-1">Asistente IA Conversacional</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
El mecánico podrá chatear con IA usando fotos de preguntas anteriores como contexto.
|
||||
Configura qué preguntas anteriores usar y el comportamiento del asistente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prompt del asistente */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
🎯 Prompt del Asistente (Rol y Comportamiento)
|
||||
</label>
|
||||
<textarea
|
||||
value={config.assistant_prompt || ''}
|
||||
onChange={(e) => updateConfig({ assistant_prompt: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
rows="6"
|
||||
placeholder="Ejemplo: Eres un experto mecánico especializado en sistemas de frenos. Ayuda al mecánico a diagnosticar problemas basándote en las fotos que has visto. Sé directo y técnico. Si ves algo anormal en las fotos, menciónalo proactivamente."
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Define cómo debe comportarse el asistente: su rol, tono, especialidad, etc.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Preguntas de contexto */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
📸 Preguntas Anteriores para Contexto
|
||||
</label>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-3">
|
||||
<p className="text-xs text-yellow-800">
|
||||
<strong>💡 Cómo funciona:</strong> El asistente verá las fotos de las preguntas que selecciones abajo.
|
||||
Elige preguntas cuyas fotos sean relevantes para el diagnóstico (ej: si es asistente de frenos, selecciona preguntas sobre pastillas, discos, líquido de frenos, etc.)
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
value={config.context_questions || ''}
|
||||
onChange={(e) => updateConfig({ context_questions: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
rows="3"
|
||||
placeholder="IDs de preguntas separados por comas. Ejemplo: 5,8,12,15
|
||||
Dejar vacío para usar TODAS las preguntas anteriores con fotos."
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Especifica los IDs de preguntas anteriores cuyas fotos debe analizar el asistente, o déjalo vacío para usar todas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Instrucciones adicionales */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
📋 Instrucciones Adicionales (Opcional)
|
||||
</label>
|
||||
<textarea
|
||||
value={config.assistant_instructions || ''}
|
||||
onChange={(e) => updateConfig({ assistant_instructions: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
rows="4"
|
||||
placeholder="Instrucciones específicas adicionales.
|
||||
Ejemplo:
|
||||
- Si detectas pastillas con menos de 3mm, recomienda cambio inmediato
|
||||
- Siempre verifica si hay fugas de líquido
|
||||
- Menciona el código OBD2 si es relevante"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Reglas o criterios específicos que el asistente debe seguir al dar consejos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Configuración de respuestas */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
💬 Mensajes Máximos
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
value={config.max_messages || 20}
|
||||
onChange={(e) => updateConfig({ max_messages: parseInt(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Límite de mensajes en el chat
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
📏 Longitud de Respuesta
|
||||
</label>
|
||||
<select
|
||||
value={config.response_length || 'medium'}
|
||||
onChange={(e) => updateConfig({ response_length: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
>
|
||||
<option value="short">Corta (concisa)</option>
|
||||
<option value="medium">Media (balanceada)</option>
|
||||
<option value="long">Larga (detallada)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user