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:
2025-12-04 11:03:19 -03:00
parent 59a0f56b99
commit 023a004c53
4 changed files with 98 additions and 25 deletions

View File

@@ -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>

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"
/>
<p className="text-xs text-indigo-300 font-medium hover:text-indigo-200">
Ayutec v1.2.2
Ayutec v1.2.3
</p>
</a>
</div>