✅ Chat AI Assistant con Archivos Adjuntos Implementado
🎯 Nueva Funcionalidad Completa Se ha implementado un sistema de chat conversacional con IA que permite adjuntar archivos (imágenes y PDFs), similar a ChatGPT, con prompt personalizable y envío completo al webhook. 📋 Características Implementadas 1. Adjuntar Archivos en el Chat ✅ Botón 📎 para adjuntar archivos ✅ Soporte para imágenes (JPG, PNG, etc.) y PDFs ✅ Preview de archivos adjuntos antes de enviar ✅ Eliminación individual de archivos adjuntos ✅ Múltiples archivos por mensaje ✅ Validación de tipos de archivo 2. Procesamiento Backend de Archivos ✅ Endpoint modificado para recibir FormData con archivos ✅ PDFs: Extracción automática de texto con pypdf ✅ Imágenes: Conversión a base64 para Vision AI ✅ Análisis combinado de texto + imágenes ✅ Límite de 2000 caracteres por PDF para optimizar 3. Integración con IA ✅ OpenAI Vision: Soporte multimodal (texto + imágenes) ✅ Gemini: Soporte de imágenes y texto ✅ Contexto enriquecido con archivos adjuntos ✅ Prompts adaptados según tipo de archivo 4. Custom Prompt por Checklist ✅ Campo assistant_prompt configurable por pregunta ✅ Campo assistant_instructions para instrucciones adicionales ✅ Control de longitud de respuesta (short/medium/long) ✅ Contexto automático del vehículo en cada mensaje 5. Persistencia del Chat ✅ Nuevo campo chat_history en modelo Answer ✅ Migración SQL: add_chat_history_to_answers.sql ✅ Guardado automático del historial completo ✅ Restauración del chat al reabrir 6. Envío al Webhook (n8n) ✅ Todos los chats incluidos en send_completed_inspection_to_n8n() ✅ Campo chat_history en cada respuesta del webhook ✅ Incluye metadata de archivos adjuntos ✅ Tipo de pregunta identificado en webhook ✅ Datos completos para análisis posterior
This commit is contained in:
@@ -131,7 +131,8 @@ def send_completed_inspection_to_n8n(inspection, db):
|
|||||||
"id": answer.question.id,
|
"id": answer.question.id,
|
||||||
"texto": answer.question.text,
|
"texto": answer.question.text,
|
||||||
"seccion": answer.question.section,
|
"seccion": answer.question.section,
|
||||||
"orden": answer.question.order
|
"orden": answer.question.order,
|
||||||
|
"tipo": answer.question.type
|
||||||
},
|
},
|
||||||
"respuesta": answer.answer_value,
|
"respuesta": answer.answer_value,
|
||||||
"estado": answer.status,
|
"estado": answer.status,
|
||||||
@@ -139,7 +140,8 @@ def send_completed_inspection_to_n8n(inspection, db):
|
|||||||
"puntos_obtenidos": answer.points_earned,
|
"puntos_obtenidos": answer.points_earned,
|
||||||
"es_critico": answer.is_flagged,
|
"es_critico": answer.is_flagged,
|
||||||
"imagenes": imagenes,
|
"imagenes": imagenes,
|
||||||
"ai_analysis": answer.ai_analysis
|
"ai_analysis": answer.ai_analysis,
|
||||||
|
"chat_history": answer.chat_history # Incluir historial de chat si existe
|
||||||
})
|
})
|
||||||
|
|
||||||
# Preparar datos completos de la inspección
|
# Preparar datos completos de la inspección
|
||||||
@@ -207,7 +209,7 @@ def send_completed_inspection_to_n8n(inspection, db):
|
|||||||
# No lanzamos excepción para no interrumpir el flujo normal
|
# No lanzamos excepción para no interrumpir el flujo normal
|
||||||
|
|
||||||
|
|
||||||
BACKEND_VERSION = "1.0.90"
|
BACKEND_VERSION = "1.0.91"
|
||||||
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||||
|
|
||||||
# S3/MinIO configuration
|
# S3/MinIO configuration
|
||||||
@@ -2959,33 +2961,80 @@ Responde en formato JSON:
|
|||||||
|
|
||||||
@app.post("/api/ai/chat-assistant")
|
@app.post("/api/ai/chat-assistant")
|
||||||
async def chat_with_ai_assistant(
|
async def chat_with_ai_assistant(
|
||||||
request: dict,
|
question_id: int = Form(...),
|
||||||
|
inspection_id: int = Form(...),
|
||||||
|
user_message: str = Form(""),
|
||||||
|
chat_history: str = Form("[]"),
|
||||||
|
context_photos: str = Form("[]"),
|
||||||
|
vehicle_info: str = Form("{}"),
|
||||||
|
assistant_prompt: str = Form(""),
|
||||||
|
assistant_instructions: str = Form(""),
|
||||||
|
response_length: str = Form("medium"),
|
||||||
|
files: List[UploadFile] = File(default=[]),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: models.User = Depends(get_current_user)
|
current_user: models.User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Chat conversacional con IA usando contexto de fotos anteriores
|
Chat conversacional con IA usando contexto de fotos anteriores
|
||||||
El asistente tiene acceso a fotos de preguntas previas para dar mejor contexto
|
El asistente tiene acceso a fotos de preguntas previas para dar mejor contexto
|
||||||
|
Ahora soporta archivos adjuntos (imágenes y PDFs)
|
||||||
"""
|
"""
|
||||||
print("\n" + "="*80)
|
print("\n" + "="*80)
|
||||||
print("🤖 AI CHAT ASSISTANT")
|
print("🤖 AI CHAT ASSISTANT")
|
||||||
print("="*80)
|
print("="*80)
|
||||||
|
|
||||||
question_id = request.get('question_id')
|
# Parsear JSON strings
|
||||||
inspection_id = request.get('inspection_id')
|
import json
|
||||||
user_message = request.get('user_message')
|
chat_history_list = json.loads(chat_history)
|
||||||
chat_history = request.get('chat_history', [])
|
context_photos_list = json.loads(context_photos)
|
||||||
context_photos = request.get('context_photos', [])
|
vehicle_info_dict = json.loads(vehicle_info)
|
||||||
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"📋 Question ID: {question_id}")
|
||||||
print(f"🚗 Inspection ID: {inspection_id}")
|
print(f"🚗 Inspection ID: {inspection_id}")
|
||||||
print(f"💬 User message: {user_message}")
|
print(f"💬 User message: {user_message}")
|
||||||
print(f"📸 Context photos: {len(context_photos)} fotos")
|
print(f"📎 Attached files: {len(files)}")
|
||||||
print(f"💭 Chat history: {len(chat_history)} mensajes previos")
|
print(f"📸 Context photos: {len(context_photos_list)} fotos")
|
||||||
|
print(f"💭 Chat history: {len(chat_history_list)} mensajes previos")
|
||||||
|
|
||||||
|
# Procesar archivos adjuntos
|
||||||
|
attached_files_data = []
|
||||||
|
if files:
|
||||||
|
import base64
|
||||||
|
from pypdf import PdfReader
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
file_content = await file.read()
|
||||||
|
file_type = file.content_type
|
||||||
|
|
||||||
|
file_info = {
|
||||||
|
'filename': file.filename,
|
||||||
|
'type': file_type,
|
||||||
|
'size': len(file_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Si es PDF, extraer texto
|
||||||
|
if file_type == 'application/pdf' or file.filename.lower().endswith('.pdf'):
|
||||||
|
try:
|
||||||
|
pdf_file = BytesIO(file_content)
|
||||||
|
pdf_reader = PdfReader(pdf_file)
|
||||||
|
pdf_text = ""
|
||||||
|
for page in pdf_reader.pages:
|
||||||
|
pdf_text += page.extract_text()
|
||||||
|
file_info['content_type'] = 'pdf'
|
||||||
|
file_info['text'] = pdf_text[:2000] # Limitar texto
|
||||||
|
print(f"📄 PDF procesado: {file.filename} - {len(pdf_text)} caracteres")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error procesando PDF {file.filename}: {str(e)}")
|
||||||
|
file_info['error'] = str(e)
|
||||||
|
|
||||||
|
# Si es imagen, convertir a base64
|
||||||
|
elif file_type.startswith('image/'):
|
||||||
|
file_info['content_type'] = 'image'
|
||||||
|
file_info['base64'] = base64.b64encode(file_content).decode('utf-8')
|
||||||
|
print(f"🖼️ Imagen procesada: {file.filename}")
|
||||||
|
|
||||||
|
attached_files_data.append(file_info)
|
||||||
|
|
||||||
# Obtener configuración de IA
|
# Obtener configuración de IA
|
||||||
ai_config = db.query(models.AIConfiguration).filter(
|
ai_config = db.query(models.AIConfiguration).filter(
|
||||||
@@ -3003,17 +3052,17 @@ async def chat_with_ai_assistant(
|
|||||||
# Construir el contexto del vehículo
|
# Construir el contexto del vehículo
|
||||||
vehicle_context = f"""
|
vehicle_context = f"""
|
||||||
INFORMACIÓN DEL VEHÍCULO:
|
INFORMACIÓN DEL VEHÍCULO:
|
||||||
- Marca: {vehicle_info.get('brand', 'N/A')}
|
- Marca: {vehicle_info_dict.get('brand', 'N/A')}
|
||||||
- Modelo: {vehicle_info.get('model', 'N/A')}
|
- Modelo: {vehicle_info_dict.get('model', 'N/A')}
|
||||||
- Placa: {vehicle_info.get('plate', 'N/A')}
|
- Placa: {vehicle_info_dict.get('plate', 'N/A')}
|
||||||
- Kilometraje: {vehicle_info.get('km', 'N/A')} km
|
- Kilometraje: {vehicle_info_dict.get('km', 'N/A')} km
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Construir el contexto de las fotos anteriores
|
# Construir el contexto de las fotos anteriores
|
||||||
photos_context = ""
|
photos_context = ""
|
||||||
if context_photos:
|
if context_photos_list:
|
||||||
photos_context = f"\n\nFOTOS ANALIZADAS PREVIAMENTE ({len(context_photos)} imágenes):\n"
|
photos_context = f"\n\nFOTOS ANALIZADAS PREVIAMENTE ({len(context_photos_list)} imágenes):\n"
|
||||||
for idx, photo in enumerate(context_photos[:10], 1): # Limitar a 10 fotos
|
for idx, photo in enumerate(context_photos_list[:10], 1): # Limitar a 10 fotos
|
||||||
ai_analysis = photo.get('aiAnalysis', [])
|
ai_analysis = photo.get('aiAnalysis', [])
|
||||||
if ai_analysis and len(ai_analysis) > 0:
|
if ai_analysis and len(ai_analysis) > 0:
|
||||||
analysis_text = ai_analysis[0].get('analysis', {})
|
analysis_text = ai_analysis[0].get('analysis', {})
|
||||||
@@ -3029,6 +3078,18 @@ INFORMACIÓN DEL VEHÍCULO:
|
|||||||
}
|
}
|
||||||
max_tokens = max_tokens_map.get(response_length, 400)
|
max_tokens = max_tokens_map.get(response_length, 400)
|
||||||
|
|
||||||
|
# Construir contexto de archivos adjuntos
|
||||||
|
attached_context = ""
|
||||||
|
if attached_files_data:
|
||||||
|
attached_context = f"\n\nARCHIVOS ADJUNTOS EN ESTE MENSAJE ({len(attached_files_data)} archivos):\n"
|
||||||
|
for idx, file_info in enumerate(attached_files_data, 1):
|
||||||
|
if file_info.get('content_type') == 'pdf':
|
||||||
|
attached_context += f"\n{idx}. PDF: {file_info['filename']}\n"
|
||||||
|
if 'text' in file_info:
|
||||||
|
attached_context += f" Contenido: {file_info['text'][:500]}...\n"
|
||||||
|
elif file_info.get('content_type') == 'image':
|
||||||
|
attached_context += f"\n{idx}. Imagen: {file_info['filename']}\n"
|
||||||
|
|
||||||
# Construir el system prompt
|
# Construir el system prompt
|
||||||
base_prompt = assistant_prompt or "Eres un experto mecánico automotriz que ayuda a diagnosticar problemas."
|
base_prompt = assistant_prompt or "Eres un experto mecánico automotriz que ayuda a diagnosticar problemas."
|
||||||
|
|
||||||
@@ -3038,6 +3099,8 @@ INFORMACIÓN DEL VEHÍCULO:
|
|||||||
|
|
||||||
{photos_context}
|
{photos_context}
|
||||||
|
|
||||||
|
{attached_context}
|
||||||
|
|
||||||
INSTRUCCIONES ADICIONALES:
|
INSTRUCCIONES ADICIONALES:
|
||||||
{assistant_instructions if assistant_instructions else "Sé técnico, claro y directo en tus respuestas."}
|
{assistant_instructions if assistant_instructions else "Sé técnico, claro y directo en tus respuestas."}
|
||||||
|
|
||||||
@@ -3053,17 +3116,39 @@ FORMATO DE RESPUESTA:
|
|||||||
messages = [{"role": "system", "content": system_prompt}]
|
messages = [{"role": "system", "content": system_prompt}]
|
||||||
|
|
||||||
# Agregar historial previo (últimos 10 mensajes para no saturar)
|
# Agregar historial previo (últimos 10 mensajes para no saturar)
|
||||||
for msg in chat_history[-10:]:
|
for msg in chat_history_list[-10:]:
|
||||||
messages.append({
|
messages.append({
|
||||||
"role": msg.get('role'),
|
"role": msg.get('role'),
|
||||||
"content": msg.get('content')
|
"content": msg.get('content')
|
||||||
})
|
})
|
||||||
|
|
||||||
# Agregar el mensaje actual del usuario
|
# Agregar el mensaje actual del usuario con imágenes si hay
|
||||||
messages.append({
|
has_images = any(f.get('content_type') == 'image' for f in attached_files_data)
|
||||||
"role": "user",
|
|
||||||
"content": user_message
|
if has_images:
|
||||||
})
|
# Formato multimodal para OpenAI/Gemini
|
||||||
|
user_content = []
|
||||||
|
if user_message:
|
||||||
|
user_content.append({"type": "text", "text": user_message})
|
||||||
|
|
||||||
|
# Agregar imágenes
|
||||||
|
for file_info in attached_files_data:
|
||||||
|
if file_info.get('content_type') == 'image':
|
||||||
|
user_content.append({
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {"url": f"data:image/jpeg;base64,{file_info['base64']}"}
|
||||||
|
})
|
||||||
|
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": user_content
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Solo texto
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": user_message
|
||||||
|
})
|
||||||
|
|
||||||
print(f"🔧 Enviando a {ai_config.provider} con {len(messages)} mensajes")
|
print(f"🔧 Enviando a {ai_config.provider} con {len(messages)} mensajes")
|
||||||
|
|
||||||
@@ -3111,7 +3196,8 @@ FORMATO DE RESPUESTA:
|
|||||||
"response": ai_response,
|
"response": ai_response,
|
||||||
"confidence": confidence,
|
"confidence": confidence,
|
||||||
"provider": ai_config.provider,
|
"provider": ai_config.provider,
|
||||||
"model": ai_config.model_name
|
"model": ai_config.model_name,
|
||||||
|
"attached_files": [{'filename': f['filename'], 'type': f['type']} for f in attached_files_data]
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ class Answer(Base):
|
|||||||
comment = Column(Text) # Comentarios adicionales
|
comment = Column(Text) # Comentarios adicionales
|
||||||
|
|
||||||
ai_analysis = Column(JSON) # Análisis de IA si aplica
|
ai_analysis = Column(JSON) # Análisis de IA si aplica
|
||||||
|
chat_history = Column(JSON) # Historial de chat con AI Assistant (para tipo ai_assistant)
|
||||||
is_flagged = Column(Boolean, default=False) # Si requiere atención
|
is_flagged = Column(Boolean, default=False) # Si requiere atención
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ class AnswerCreate(AnswerBase):
|
|||||||
inspection_id: int
|
inspection_id: int
|
||||||
question_id: int
|
question_id: int
|
||||||
ai_analysis: Optional[list] = None # Lista de análisis de IA (soporta múltiples imágenes)
|
ai_analysis: Optional[list] = None # Lista de análisis de IA (soporta múltiples imágenes)
|
||||||
|
chat_history: Optional[list] = None # Historial de chat con AI Assistant
|
||||||
|
|
||||||
class AnswerUpdate(AnswerBase):
|
class AnswerUpdate(AnswerBase):
|
||||||
pass
|
pass
|
||||||
@@ -225,6 +226,7 @@ class Answer(AnswerBase):
|
|||||||
question_id: int
|
question_id: int
|
||||||
points_earned: int
|
points_earned: int
|
||||||
ai_analysis: Optional[list] = None # Lista de análisis de IA
|
ai_analysis: Optional[list] = None # Lista de análisis de IA
|
||||||
|
chat_history: Optional[list] = None # Historial de chat con AI Assistant
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.92",
|
"version": "1.0.93",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Service Worker para PWA con detección de actualizaciones
|
// Service Worker para PWA con detección de actualizaciones
|
||||||
// IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión
|
// IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión
|
||||||
const CACHE_NAME = 'ayutec-v1.0.92';
|
const CACHE_NAME = 'ayutec-v1.0.93';
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html'
|
'/index.html'
|
||||||
|
|||||||
@@ -4270,7 +4270,8 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
|||||||
status: status,
|
status: status,
|
||||||
comment: answer.observations || null,
|
comment: answer.observations || null,
|
||||||
ai_analysis: answer.aiAnalysis || null,
|
ai_analysis: answer.aiAnalysis || null,
|
||||||
is_flagged: status === 'critical'
|
is_flagged: status === 'critical',
|
||||||
|
chat_history: answer.chatHistory || null // Agregar historial de chat
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/api/answers`, {
|
const response = await fetch(`${API_URL}/api/answers`, {
|
||||||
@@ -5265,7 +5266,9 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
|||||||
// Componente Modal de Chat IA Asistente
|
// Componente Modal de Chat IA Asistente
|
||||||
function AIAssistantChatModal({ question, inspection, allAnswers, messages, setMessages, loading, setLoading, onClose }) {
|
function AIAssistantChatModal({ question, inspection, allAnswers, messages, setMessages, loading, setLoading, onClose }) {
|
||||||
const [inputMessage, setInputMessage] = useState('')
|
const [inputMessage, setInputMessage] = useState('')
|
||||||
|
const [attachedFiles, setAttachedFiles] = useState([])
|
||||||
const chatEndRef = useRef(null)
|
const chatEndRef = useRef(null)
|
||||||
|
const fileInputRef = useRef(null)
|
||||||
const config = question.options || {}
|
const config = question.options || {}
|
||||||
|
|
||||||
// Auto-scroll al final
|
// Auto-scroll al final
|
||||||
@@ -5273,19 +5276,66 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
// Manejar adjuntos de archivos
|
||||||
|
const handleFileAttach = (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
const validFiles = files.filter(file => {
|
||||||
|
const isImage = file.type.startsWith('image/')
|
||||||
|
const isPDF = file.type === 'application/pdf'
|
||||||
|
const isValid = isImage || isPDF
|
||||||
|
if (!isValid) {
|
||||||
|
alert(`⚠️ ${file.name}: Solo se permiten imágenes y PDFs`)
|
||||||
|
}
|
||||||
|
return isValid
|
||||||
|
})
|
||||||
|
setAttachedFiles(prev => [...prev, ...validFiles])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAttachedFile = (index) => {
|
||||||
|
setAttachedFiles(prev => prev.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
|
||||||
// Enviar mensaje al asistente
|
// Enviar mensaje al asistente
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
if (!inputMessage.trim() || loading) return
|
if ((!inputMessage.trim() && attachedFiles.length === 0) || loading) return
|
||||||
|
|
||||||
const userMessage = { role: 'user', content: inputMessage, timestamp: new Date().toISOString() }
|
const userMessage = {
|
||||||
|
role: 'user',
|
||||||
|
content: inputMessage || '📎 Archivos adjuntos',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
files: attachedFiles.map(f => ({ name: f.name, type: f.type, size: f.size }))
|
||||||
|
}
|
||||||
setMessages(prev => [...prev, userMessage])
|
setMessages(prev => [...prev, userMessage])
|
||||||
|
|
||||||
|
const currentFiles = attachedFiles
|
||||||
setInputMessage('')
|
setInputMessage('')
|
||||||
|
setAttachedFiles([])
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||||
|
|
||||||
|
// Preparar FormData para enviar archivos
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('question_id', question.id)
|
||||||
|
formData.append('inspection_id', inspection.id)
|
||||||
|
formData.append('user_message', inputMessage)
|
||||||
|
formData.append('chat_history', JSON.stringify(messages))
|
||||||
|
formData.append('assistant_prompt', config.assistant_prompt || '')
|
||||||
|
formData.append('assistant_instructions', config.assistant_instructions || '')
|
||||||
|
formData.append('response_length', config.response_length || 'medium')
|
||||||
|
|
||||||
|
// Adjuntar archivos
|
||||||
|
currentFiles.forEach((file, index) => {
|
||||||
|
formData.append('files', file)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Adjuntar archivos
|
||||||
|
currentFiles.forEach((file, index) => {
|
||||||
|
formData.append('files', file)
|
||||||
|
})
|
||||||
|
|
||||||
// Recopilar fotos de preguntas anteriores según configuración
|
// Recopilar fotos de preguntas anteriores según configuración
|
||||||
const contextPhotos = []
|
const contextPhotos = []
|
||||||
const contextQuestionIds = config.context_questions
|
const contextQuestionIds = config.context_questions
|
||||||
@@ -5308,33 +5358,23 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Preparar el payload
|
formData.append('context_photos', JSON.stringify(contextPhotos))
|
||||||
const payload = {
|
formData.append('vehicle_info', JSON.stringify({
|
||||||
question_id: question.id,
|
brand: inspection.vehicle_brand,
|
||||||
inspection_id: inspection.id,
|
model: inspection.vehicle_model,
|
||||||
user_message: inputMessage,
|
plate: inspection.vehicle_plate,
|
||||||
chat_history: messages,
|
km: inspection.vehicle_km
|
||||||
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)
|
console.log('📤 Enviando a chat IA con archivos:', currentFiles.length)
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/api/ai/chat-assistant`, {
|
const response = await fetch(`${API_URL}/api/ai/chat-assistant`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`
|
||||||
'Content-Type': 'application/json'
|
// No incluir Content-Type, fetch lo establece automáticamente con FormData
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: formData
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -5430,6 +5470,18 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
<div className="text-sm sm:text-base whitespace-pre-wrap break-words">
|
<div className="text-sm sm:text-base whitespace-pre-wrap break-words">
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Mostrar archivos adjuntos si existen */}
|
||||||
|
{msg.files && msg.files.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{msg.files.map((file, fIdx) => (
|
||||||
|
<div key={fIdx} className={`text-xs flex items-center gap-1 ${msg.role === 'user' ? 'text-blue-100' : 'text-gray-600'}`}>
|
||||||
|
<span>{file.type.startsWith('image/') ? '🖼️' : '📄'}</span>
|
||||||
|
<span className="truncate">{file.name}</span>
|
||||||
|
<span>({(file.size / 1024).toFixed(1)} KB)</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={`text-xs mt-2 ${
|
className={`text-xs mt-2 ${
|
||||||
msg.role === 'user' ? 'text-blue-100' : 'text-gray-400'
|
msg.role === 'user' ? 'text-blue-100' : 'text-gray-400'
|
||||||
@@ -5463,7 +5515,43 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
|
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
<div className="border-t p-3 sm:p-4 bg-white rounded-b-xl">
|
<div className="border-t p-3 sm:p-4 bg-white rounded-b-xl">
|
||||||
|
{/* Preview de archivos adjuntos */}
|
||||||
|
{attachedFiles.length > 0 && (
|
||||||
|
<div className="mb-3 flex flex-wrap gap-2">
|
||||||
|
{attachedFiles.map((file, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-lg text-sm">
|
||||||
|
<span>{file.type.startsWith('image/') ? '🖼️' : '📄'}</span>
|
||||||
|
<span className="max-w-[150px] truncate">{file.name}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeAttachedFile(idx)}
|
||||||
|
className="text-red-600 hover:text-red-800 font-bold"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileAttach}
|
||||||
|
accept="image/*,application/pdf"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-3 py-2 sm:py-3 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Adjuntar archivos (imágenes o PDFs)"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
📎
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={inputMessage}
|
value={inputMessage}
|
||||||
@@ -5475,7 +5563,7 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={sendMessage}
|
onClick={sendMessage}
|
||||||
disabled={!inputMessage.trim() || loading}
|
disabled={(!inputMessage.trim() && attachedFiles.length === 0) || 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"
|
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
|
Enviar
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<div className="mb-3 px-2 py-1.5 bg-indigo-900/30 rounded-lg border border-indigo-700/30">
|
<div className="mb-3 px-2 py-1.5 bg-indigo-900/30 rounded-lg border border-indigo-700/30">
|
||||||
<p className="text-xs text-indigo-300 text-center font-medium">
|
<p className="text-xs text-indigo-300 text-center font-medium">
|
||||||
Ayutec v1.0.92
|
Ayutec v1.0.93
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
6
migrations/add_chat_history_to_answers.sql
Normal file
6
migrations/add_chat_history_to_answers.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- Agregar campo chat_history a la tabla answers
|
||||||
|
-- Fecha: 2025-12-02
|
||||||
|
|
||||||
|
ALTER TABLE answers ADD COLUMN IF NOT EXISTS chat_history JSON;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN answers.chat_history IS 'Historial de conversación con AI Assistant para preguntas tipo ai_assistant';
|
||||||
Reference in New Issue
Block a user