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:
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user