✅ 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:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"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.2.2';
|
const CACHE_NAME = 'ayutec-v1.2.3';
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html'
|
'/index.html'
|
||||||
|
|||||||
@@ -5466,6 +5466,17 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
}, [messages])
|
}, [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
|
// Manejar adjuntos de archivos
|
||||||
const handleFileAttach = (e) => {
|
const handleFileAttach = (e) => {
|
||||||
const files = Array.from(e.target.files)
|
const files = Array.from(e.target.files)
|
||||||
@@ -5478,11 +5489,28 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
}
|
}
|
||||||
return isValid
|
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) => {
|
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
|
// Enviar mensaje al asistente
|
||||||
@@ -5493,7 +5521,12 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
role: 'user',
|
role: 'user',
|
||||||
content: inputMessage || '📎 Archivos adjuntos',
|
content: inputMessage || '📎 Archivos adjuntos',
|
||||||
timestamp: new Date().toISOString(),
|
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])
|
setMessages(prev => [...prev, userMessage])
|
||||||
|
|
||||||
@@ -5516,9 +5549,9 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
formData.append('assistant_instructions', config.assistant_instructions || '')
|
formData.append('assistant_instructions', config.assistant_instructions || '')
|
||||||
formData.append('response_length', config.response_length || 'medium')
|
formData.append('response_length', config.response_length || 'medium')
|
||||||
|
|
||||||
// Adjuntar archivos
|
// Adjuntar archivos (extraer el objeto File del wrapper)
|
||||||
currentFiles.forEach((file, index) => {
|
currentFiles.forEach((fileWrapper, index) => {
|
||||||
formData.append('files', file)
|
formData.append('files', fileWrapper.file || fileWrapper)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Recopilar fotos de preguntas anteriores según configuración
|
// Recopilar fotos de preguntas anteriores según configuración
|
||||||
@@ -5674,13 +5707,31 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
)}
|
)}
|
||||||
{/* Mostrar archivos adjuntos si existen */}
|
{/* Mostrar archivos adjuntos si existen */}
|
||||||
{msg.files && msg.files.length > 0 && (
|
{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) => (
|
{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'}`}>
|
<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>{file.type.startsWith('image/') ? '🖼️' : '📄'}</span>
|
||||||
<span className="truncate">{file.name}</span>
|
<span className="truncate">{file.name}</span>
|
||||||
<span>({(file.size / 1024).toFixed(1)} KB)</span>
|
<span>({(file.size / 1024).toFixed(1)} KB)</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -5720,10 +5771,30 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
{/* Preview de archivos adjuntos */}
|
{/* Preview de archivos adjuntos */}
|
||||||
{attachedFiles.length > 0 && (
|
{attachedFiles.length > 0 && (
|
||||||
<div className="mb-3 flex flex-wrap gap-2">
|
<div className="mb-3 flex flex-wrap gap-2">
|
||||||
{attachedFiles.map((file, idx) => (
|
{attachedFiles.map((fileWrapper, idx) => (
|
||||||
<div key={idx} className="flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-lg text-sm">
|
<div key={idx} className="relative bg-gray-100 rounded-lg overflow-hidden">
|
||||||
<span>{file.type.startsWith('image/') ? '🖼️' : '📄'}</span>
|
{fileWrapper.preview ? (
|
||||||
<span className="max-w-[150px] truncate">{file.name}</span>
|
<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
|
<button
|
||||||
onClick={() => removeAttachedFile(idx)}
|
onClick={() => removeAttachedFile(idx)}
|
||||||
className="text-red-600 hover:text-red-800 font-bold"
|
className="text-red-600 hover:text-red-800 font-bold"
|
||||||
@@ -5732,6 +5803,8 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
|
|||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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.2.2
|
Ayutec v1.2.3
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user