|
|
|
|
@@ -4384,6 +4384,7 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
|
|
|
|
const [showAIChat, setShowAIChat] = useState(false)
|
|
|
|
|
const [aiChatMessages, setAiChatMessages] = useState([])
|
|
|
|
|
const [aiChatLoading, setAiChatLoading] = useState(false)
|
|
|
|
|
const [initialMessageSent, setInitialMessageSent] = useState(false)
|
|
|
|
|
|
|
|
|
|
// Signature canvas
|
|
|
|
|
const mechanicSigRef = useRef(null)
|
|
|
|
|
@@ -5309,9 +5310,10 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const existingHistory = answers[currentQuestion.id]?.chatHistory || []
|
|
|
|
|
setAiChatMessages(existingHistory)
|
|
|
|
|
setInitialMessageSent(existingHistory.length > 0) // Solo enviar inicial si no hay historial
|
|
|
|
|
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"
|
|
|
|
|
>
|
|
|
|
|
@@ -5647,12 +5649,14 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
|
|
|
|
{showAIChat && currentQuestion && (
|
|
|
|
|
<AIAssistantChatModal
|
|
|
|
|
question={currentQuestion}
|
|
|
|
|
inspection={{ id: inspectionId, ...vehicleData }}
|
|
|
|
|
inspection={{ id: inspectionId, ...vehicleData, checklist: checklist }}
|
|
|
|
|
allAnswers={answers}
|
|
|
|
|
messages={aiChatMessages}
|
|
|
|
|
setMessages={setAiChatMessages}
|
|
|
|
|
loading={aiChatLoading}
|
|
|
|
|
setLoading={setAiChatLoading}
|
|
|
|
|
initialMessageSent={initialMessageSent}
|
|
|
|
|
setInitialMessageSent={setInitialMessageSent}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setShowAIChat(false)
|
|
|
|
|
// Guardar historial del chat en la respuesta
|
|
|
|
|
@@ -5673,7 +5677,7 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 [attachedFiles, setAttachedFiles] = useState([])
|
|
|
|
|
const [selectedImage, setSelectedImage] = useState(null) // Para lightbox
|
|
|
|
|
@@ -5686,6 +5690,14 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|
|
|
|
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
|
|
|
}, [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
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
@@ -5697,6 +5709,122 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|
|
|
|
}
|
|
|
|
|
}, [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
|
|
|
|
|
const handleFileAttach = (e) => {
|
|
|
|
|
const files = Array.from(e.target.files)
|
|
|
|
|
|