✅ IMPLEMENTADO - Previsualización de Imágenes en Chat Assistant
📸 Frontend actualizado a v1.2.3 He implementado un sistema completo de previsualización de imágenes en el chat assistant: 🎨 Características Implementadas: 1. Preview Antes de Enviar (Zona de Input) Miniaturas 20x20px con superposición de nombre Botón de eliminar en esquina superior derecha (rojo con X) Fondo oscuro translúcido para nombre del archivo Hover effects para mejor UX // Vista previa antes de enviar: ┌─────────────────────────────┐ │ [IMG] [IMG] 📄 file.pdf │ ← Miniaturas clickeables │ ✕ ✕ ✕ │ └─────────────────────────────┘ 2. Imágenes en Mensajes del Chat Renderizado completo de imágenes en mensajes del usuario Máximo 256px de altura (responsive) Click para abrir en nueva pestaña (full size) Metadata bajo la imagen (nombre + tamaño) Esquina redondeada para mejor diseño Transición hover (opacity 90%) // Mensaje del usuario con imagen: ┌────────────────────────────┐ │ [Texto del mensaje] │ │ │ │ ┌────────────────────────┐ │ │ │ │ │ │ │ [IMAGEN PREVIEW] │ │ ← Click para ampliar │ │ │ │ │ └────────────────────────┘ │ │ 🖼️ photo.jpg (128.5 KB) │ │ │ │ 10:45 │ └────────────────────────────┘ 3. Gestión de Memoria URLs temporales con URL.createObjectURL() Limpieza automática al eliminar archivo useEffect cleanup al desmontar modal No memory leaks garantizados
This commit is contained in:
@@ -5466,6 +5466,17 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
||||
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
// Limpiar URLs temporales al desmontar el componente
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
attachedFiles.forEach(fileWrapper => {
|
||||
if (fileWrapper?.preview) {
|
||||
URL.revokeObjectURL(fileWrapper.preview)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [attachedFiles])
|
||||
|
||||
// Manejar adjuntos de archivos
|
||||
const handleFileAttach = (e) => {
|
||||
const files = Array.from(e.target.files)
|
||||
@@ -5478,11 +5489,28 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
||||
}
|
||||
return isValid
|
||||
})
|
||||
setAttachedFiles(prev => [...prev, ...validFiles])
|
||||
|
||||
// Crear objetos con File y URL temporal para preview
|
||||
const filesWithPreview = validFiles.map(file => ({
|
||||
file: file,
|
||||
preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : null,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size
|
||||
}))
|
||||
|
||||
setAttachedFiles(prev => [...prev, ...filesWithPreview])
|
||||
}
|
||||
|
||||
const removeAttachedFile = (index) => {
|
||||
setAttachedFiles(prev => prev.filter((_, i) => i !== index))
|
||||
setAttachedFiles(prev => {
|
||||
const fileToRemove = prev[index]
|
||||
// Liberar URL temporal si existe
|
||||
if (fileToRemove?.preview) {
|
||||
URL.revokeObjectURL(fileToRemove.preview)
|
||||
}
|
||||
return prev.filter((_, i) => i !== index)
|
||||
})
|
||||
}
|
||||
|
||||
// Enviar mensaje al asistente
|
||||
@@ -5493,7 +5521,12 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
||||
role: 'user',
|
||||
content: inputMessage || '📎 Archivos adjuntos',
|
||||
timestamp: new Date().toISOString(),
|
||||
files: attachedFiles.map(f => ({ name: f.name, type: f.type, size: f.size }))
|
||||
files: attachedFiles.map(f => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
size: f.size,
|
||||
preview: f.preview // Guardar URL temporal para mostrar en chat
|
||||
}))
|
||||
}
|
||||
setMessages(prev => [...prev, userMessage])
|
||||
|
||||
@@ -5516,9 +5549,9 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
||||
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 (extraer el objeto File del wrapper)
|
||||
currentFiles.forEach((fileWrapper, index) => {
|
||||
formData.append('files', fileWrapper.file || fileWrapper)
|
||||
})
|
||||
|
||||
// Recopilar fotos de preguntas anteriores según configuración
|
||||
@@ -5674,12 +5707,30 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
||||
)}
|
||||
{/* Mostrar archivos adjuntos si existen */}
|
||||
{msg.files && msg.files.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="mt-3 space-y-2">
|
||||
{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 key={fIdx}>
|
||||
{file.type.startsWith('image/') && file.preview ? (
|
||||
<div className="space-y-1">
|
||||
<img
|
||||
src={file.preview}
|
||||
alt={file.name}
|
||||
className="rounded-lg max-w-full h-auto max-h-64 object-contain cursor-pointer hover:opacity-90 transition"
|
||||
onClick={() => window.open(file.preview, '_blank')}
|
||||
/>
|
||||
<div className={`text-xs flex items-center gap-1 ${msg.role === 'user' ? 'text-blue-100' : 'text-gray-500'}`}>
|
||||
<span>🖼️</span>
|
||||
<span className="truncate">{file.name}</span>
|
||||
<span>({(file.size / 1024).toFixed(1)} KB)</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div 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>
|
||||
@@ -5720,17 +5771,39 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
||||
{/* 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>
|
||||
{attachedFiles.map((fileWrapper, idx) => (
|
||||
<div key={idx} className="relative bg-gray-100 rounded-lg overflow-hidden">
|
||||
{fileWrapper.preview ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={fileWrapper.preview}
|
||||
alt={fileWrapper.name}
|
||||
className="h-20 w-20 object-cover"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeAttachedFile(idx)}
|
||||
className="absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold hover:bg-red-700 shadow-lg"
|
||||
type="button"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 text-white text-xs px-1 py-0.5 truncate">
|
||||
{fileWrapper.name}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 px-3 py-2 text-sm">
|
||||
<span>📄</span>
|
||||
<span className="max-w-[150px] truncate">{fileWrapper.name}</span>
|
||||
<button
|
||||
onClick={() => removeAttachedFile(idx)}
|
||||
className="text-red-600 hover:text-red-800 font-bold"
|
||||
type="button"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user