Backend v1.2.7 / Frontend v1.3.0

Problema de blob URLs solucionado:

Cambios implementados:
Backend:

Las imágenes del chat ahora se suben a S3 automáticamente
Se genera una URL permanente para cada imagen
La respuesta incluye url para cada archivo adjunto
Frontend:

Las imágenes en mensajes del asistente usan URLs de S3 (permanentes)
Ya no depende de blob URLs que desaparecen al refrescar
Las imágenes del usuario siguen usando blob (temporal solo para preview)
Las imágenes del asistente se muestran con URLs persistentes
Resultado:

 Las imágenes del chat ahora funcionan después de refrescar
 Las URLs son permanentes y accesibles
 No más errores 404 con blob: URLs
 Las imágenes quedan guardadas en S3 para historial
This commit is contained in:
2025-12-04 16:47:00 -03:00
parent 387897acfc
commit ae3a50054a
5 changed files with 89 additions and 9 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "checklist-frontend",
"private": true,
"version": "1.2.8",
"version": "1.3.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,6 +1,6 @@
// Service Worker para PWA con detección de actualizaciones
// IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión
const CACHE_NAME = 'ayutec-v1.2.8';
const CACHE_NAME = 'ayutec-v1.3.0';
const urlsToCache = [
'/',
'/index.html'

View File

@@ -5656,12 +5656,31 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
// NUEVO: Agregar respuestas de texto (incluyendo observations)
if (answer?.value || answer?.observations) {
// Buscar la pregunta para obtener su texto
// Buscar la pregunta para obtener su texto y tipo
const questionData = inspection.checklist.questions.find(q => q.id === qId)
// Formatear respuesta según el tipo de pregunta
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) {
// Buscar la opción seleccionada
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})`
}
// Para text, number, date, time, multiple_choice, photo_only: usar valor directo
}
contextAnswers.push({
questionId: qId,
questionText: questionData?.text || `Pregunta ${qId}`,
answer: answer.value || '',
questionType: questionData?.options?.type || 'text',
answer: formattedAnswer,
observations: answer.observations || ''
})
}
@@ -5694,11 +5713,17 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
const data = await response.json()
console.log('📥 Respuesta de IA:', data)
// Crear mensaje del asistente con archivos adjuntos (usando URLs del servidor)
const assistantMessage = {
role: 'assistant',
content: data.response,
timestamp: new Date().toISOString(),
confidence: data.confidence
confidence: data.confidence,
attachedFiles: data.attached_files?.map(f => ({
filename: f.filename,
type: f.type,
url: f.url // URL de S3, no blob
})) || []
}
setMessages(prev => [...prev, assistantMessage])
@@ -5827,6 +5852,34 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
))}
</div>
)}
{/* Mostrar archivos del asistente (URLs de S3) */}
{msg.attachedFiles && msg.attachedFiles.length > 0 && (
<div className="mt-3 space-y-2">
{msg.attachedFiles.map((file, fIdx) => (
<div key={fIdx}>
{file.type.startsWith('image/') && file.url ? (
<div className="space-y-1">
<img
src={file.url}
alt={file.filename}
className="rounded-lg max-w-full h-auto max-h-64 object-contain cursor-zoom-in hover:opacity-90 transition border border-gray-300"
onClick={() => setSelectedImage({ url: file.url, name: file.filename })}
/>
<div className="text-xs flex items-center gap-1 text-gray-500">
<span>🖼</span>
<span className="truncate">{file.filename}</span>
</div>
</div>
) : (
<div className="text-xs flex items-center gap-1 text-gray-600">
<span>{file.type.startsWith('image/') ? '🖼️' : '📄'}</span>
<span className="truncate">{file.filename}</span>
</div>
)}
</div>
))}
</div>
)}
<div
className={`text-xs mt-2 ${
msg.role === 'user' ? 'text-blue-100' : 'text-gray-400'

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.8
Ayutec v1.3.0
</p>
</a>
</div>