Versiones Actualizadas

Backend: 1.2.12 → 1.2.13
Frontend: 1.3.8 → 1.3.9
💡 Beneficios
Mejor UX: El mecánico no ve un chat vacío, sino orientación inmediata
Contextualizado: El asistente menciona lo que ya conoce de la inspección
Guía clara: Indica qué puede hacer el asistente según las instrucciones configuradas
Eficiente: Solo se genera una vez por sesión de chat
This commit is contained in:
2025-12-08 16:26:59 -03:00
parent b3c0030a52
commit 27763bf155
5 changed files with 171 additions and 8 deletions

View File

@@ -278,7 +278,7 @@ def extract_pdf_text_smart(pdf_content: bytes, max_chars: int = None) -> dict:
} }
BACKEND_VERSION = "1.2.12" BACKEND_VERSION = "1.2.13"
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
# S3/MinIO configuration # S3/MinIO configuration
@@ -3453,6 +3453,11 @@ async def chat_with_ai_assistant(
print(f"📝 Context answers: {len(context_answers_list)} respuestas previas") print(f"📝 Context answers: {len(context_answers_list)} respuestas previas")
print(f"💭 Chat history: {len(chat_history_list)} mensajes previos") print(f"💭 Chat history: {len(chat_history_list)} mensajes previos")
# Detectar si es el mensaje inicial de bienvenida
is_initial_greeting = (user_message == "__INITIAL_GREETING__")
if is_initial_greeting:
print("🎉 MENSAJE INICIAL DE BIENVENIDA")
# Procesar archivos adjuntos # Procesar archivos adjuntos
attached_files_data = [] attached_files_data = []
if files: if files:
@@ -3564,6 +3569,36 @@ INFORMACIÓN DEL VEHÍCULO:
if observations: if observations:
answers_context += f" Observaciones: {observations}\n" answers_context += f" Observaciones: {observations}\n"
# Si es mensaje inicial, generar saludo contextualizado
if is_initial_greeting:
greeting_parts = ["¡Hola! Soy tu Asistente Ayutec."]
# Mencionar instrucciones específicas si existen
if assistant_instructions:
greeting_parts.append(f"\n\n{assistant_instructions}")
elif assistant_prompt:
greeting_parts.append(f"\n\nEstoy aquí para ayudarte con: {assistant_prompt}")
# Mencionar contexto disponible
context_info = []
if context_answers_list:
context_info.append(f"{len(context_answers_list)} respuestas anteriores")
if context_photos_list:
context_info.append(f"{len(context_photos_list)} fotografías")
if context_info:
greeting_parts.append(f"\n\nHe analizado {' y '.join(context_info)} de esta inspección del vehículo {vehicle_info_dict.get('brand', '')} {vehicle_info_dict.get('model', '')} (Placa: {vehicle_info_dict.get('plate', '')}).")
# Pregunta específica o solicitud de información
if assistant_prompt:
greeting_parts.append("\n\n¿Qué información necesitas o deseas que analice?")
else:
greeting_parts.append("\n\n¿En qué puedo ayudarte con esta inspección?")
# Reemplazar el mensaje especial con el saludo generado
user_message = "".join(greeting_parts)
print(f"✅ Mensaje de bienvenida generado: {user_message[:100]}...")
# Definir la longitud de respuesta # Definir la longitud de respuesta
max_tokens_map = { max_tokens_map = {
'short': 200, 'short': 200,

View File

@@ -1,7 +1,7 @@
{ {
"name": "checklist-frontend", "name": "checklist-frontend",
"private": true, "private": true,
"version": "1.3.8", "version": "1.3.9",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -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.3.8'; const CACHE_NAME = 'ayutec-v1.3.9';
const urlsToCache = [ const urlsToCache = [
'/', '/',
'/index.html' '/index.html'

View File

@@ -4384,6 +4384,7 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
const [showAIChat, setShowAIChat] = useState(false) const [showAIChat, setShowAIChat] = useState(false)
const [aiChatMessages, setAiChatMessages] = useState([]) const [aiChatMessages, setAiChatMessages] = useState([])
const [aiChatLoading, setAiChatLoading] = useState(false) const [aiChatLoading, setAiChatLoading] = useState(false)
const [initialMessageSent, setInitialMessageSent] = useState(false)
// Signature canvas // Signature canvas
const mechanicSigRef = useRef(null) const mechanicSigRef = useRef(null)
@@ -5309,9 +5310,10 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
const existingHistory = answers[currentQuestion.id]?.chatHistory || []
setAiChatMessages(existingHistory)
setInitialMessageSent(existingHistory.length > 0) // Solo enviar inicial si no hay historial
setShowAIChat(true) setShowAIChat(true)
// SIEMPRE inicializar - cargar historial guardado O array vacío para nueva sesión
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" 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"
> >
@@ -5647,12 +5649,14 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
{showAIChat && currentQuestion && ( {showAIChat && currentQuestion && (
<AIAssistantChatModal <AIAssistantChatModal
question={currentQuestion} question={currentQuestion}
inspection={{ id: inspectionId, ...vehicleData }} inspection={{ id: inspectionId, ...vehicleData, checklist: checklist }}
allAnswers={answers} allAnswers={answers}
messages={aiChatMessages} messages={aiChatMessages}
setMessages={setAiChatMessages} setMessages={setAiChatMessages}
loading={aiChatLoading} loading={aiChatLoading}
setLoading={setAiChatLoading} setLoading={setAiChatLoading}
initialMessageSent={initialMessageSent}
setInitialMessageSent={setInitialMessageSent}
onClose={() => { onClose={() => {
setShowAIChat(false) setShowAIChat(false)
// Guardar historial del chat en la respuesta // Guardar historial del chat en la respuesta
@@ -5673,7 +5677,7 @@ 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, initialMessageSent, setInitialMessageSent, onClose }) {
const [inputMessage, setInputMessage] = useState('') const [inputMessage, setInputMessage] = useState('')
const [attachedFiles, setAttachedFiles] = useState([]) const [attachedFiles, setAttachedFiles] = useState([])
const [selectedImage, setSelectedImage] = useState(null) // Para lightbox const [selectedImage, setSelectedImage] = useState(null) // Para lightbox
@@ -5686,6 +5690,14 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }) chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages]) }, [messages])
// Generar mensaje de bienvenida automático al abrir el chat
useEffect(() => {
if (!initialMessageSent && messages.length === 0) {
sendInitialMessage()
setInitialMessageSent(true)
}
}, [initialMessageSent])
// Limpiar URLs temporales al desmontar el componente // Limpiar URLs temporales al desmontar el componente
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -5697,6 +5709,122 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
} }
}, [attachedFiles]) }, [attachedFiles])
// Enviar mensaje inicial de bienvenida del asistente
const sendInitialMessage = async () => {
setLoading(true)
try {
const token = localStorage.getItem('token')
const API_URL = import.meta.env.VITE_API_URL || ''
// Preparar contexto para el mensaje inicial
const contextPhotos = []
const contextAnswers = []
const config = question.options || {}
const contextQuestionIds = config.context_questions
? config.context_questions.split(',').map(id => parseInt(id.trim()))
: Object.keys(allAnswers).map(id => parseInt(id))
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
})
})
}
if (answer?.value || answer?.observations) {
const questionData = inspection?.checklist?.questions?.find(q => q.id === qId)
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) {
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})`
}
}
contextAnswers.push({
questionId: qId,
questionText: questionData?.text || `Pregunta ${qId}`,
questionType: questionData?.options?.type || 'text',
answer: formattedAnswer,
observations: answer.observations || ''
})
}
})
const formData = new FormData()
formData.append('question_id', question.id)
formData.append('inspection_id', inspection.id)
formData.append('user_message', '__INITIAL_GREETING__') // Mensaje especial para identificar inicio
formData.append('chat_history', JSON.stringify([]))
formData.append('assistant_prompt', config.assistant_prompt || '')
formData.append('assistant_instructions', config.assistant_instructions || '')
formData.append('response_length', config.response_length || 'medium')
formData.append('context_photos', JSON.stringify(contextPhotos))
formData.append('context_answers', JSON.stringify(contextAnswers))
formData.append('vehicle_info', JSON.stringify({
brand: inspection.vehicle_brand,
model: inspection.vehicle_model,
plate: inspection.vehicle_plate,
km: inspection.vehicle_km
}))
console.log('📤 Generando mensaje de bienvenida del asistente...')
const response = await fetch(`${API_URL}/api/ai/chat-assistant`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
})
if (!response.ok) {
throw new Error('Error en respuesta del servidor')
}
const data = await response.json()
// Crear solo el mensaje del asistente (sin mensaje del usuario)
const assistantMessage = {
role: 'assistant',
content: data.response,
timestamp: new Date().toISOString(),
confidence: data.confidence
}
setMessages([assistantMessage])
} catch (error) {
console.error('Error al generar mensaje inicial:', error)
const errorMessage = {
role: 'assistant',
content: '❌ Error al inicializar el chat. Por favor intenta nuevamente.',
timestamp: new Date().toISOString(),
isError: true
}
setMessages([errorMessage])
} finally {
setLoading(false)
}
}
// Manejar adjuntos de archivos // Manejar adjuntos de archivos
const handleFileAttach = (e) => { const handleFileAttach = (e) => {
const files = Array.from(e.target.files) const files = Array.from(e.target.files)

View File

@@ -153,7 +153,7 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
className="w-10 h-10 object-contain bg-white rounded p-1" className="w-10 h-10 object-contain bg-white rounded p-1"
/> />
<p className="text-xs text-indigo-300 font-medium hover:text-indigo-200"> <p className="text-xs text-indigo-300 font-medium hover:text-indigo-200">
Ayutec v1.3.8 Ayutec v1.3.9
</p> </p>
</a> </a>
</div> </div>