vista de pdf
This commit is contained in:
@@ -3,22 +3,23 @@ import { useFileStore } from '@/stores/fileStore'
|
||||
import { api } from '@/services/api'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@/components/ui/table'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { FileUpload } from './FileUpload'
|
||||
import { DeleteConfirmDialog } from './DeleteConfirmDialog'
|
||||
import {
|
||||
Upload,
|
||||
Download,
|
||||
Trash2,
|
||||
Search,
|
||||
import { PDFPreviewModal } from './PDFPreviewModal'
|
||||
import {
|
||||
Upload,
|
||||
Download,
|
||||
Trash2,
|
||||
Search,
|
||||
FileText,
|
||||
Eye,
|
||||
MessageSquare
|
||||
@@ -44,6 +45,13 @@ export function Dashboard() {
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
|
||||
// Estados para el modal de preview de PDF
|
||||
const [previewModalOpen, setPreviewModalOpen] = useState(false)
|
||||
const [previewFileUrl, setPreviewFileUrl] = useState<string | null>(null)
|
||||
const [previewFileName, setPreviewFileName] = useState('')
|
||||
const [previewFileTema, setPreviewFileTema] = useState<string | undefined>(undefined)
|
||||
const [loadingPreview, setLoadingPreview] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadFiles()
|
||||
}, [selectedTema])
|
||||
@@ -119,7 +127,7 @@ export function Dashboard() {
|
||||
// Descargar archivos seleccionados
|
||||
const handleDownloadMultiple = async () => {
|
||||
if (selectedFiles.size === 0) return
|
||||
|
||||
|
||||
try {
|
||||
setDownloading(true)
|
||||
const filesToDownload = Array.from(selectedFiles)
|
||||
@@ -132,6 +140,39 @@ export function Dashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
// Abrir preview de PDF
|
||||
const handlePreviewFile = async (filename: string, tema?: string) => {
|
||||
// Solo permitir preview de archivos PDF
|
||||
if (!filename.toLowerCase().endsWith('.pdf')) {
|
||||
console.log('Solo se pueden previsualizar archivos PDF')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoadingPreview(true)
|
||||
setPreviewFileName(filename)
|
||||
setPreviewFileTema(tema)
|
||||
|
||||
// Obtener la URL temporal (SAS) para el archivo
|
||||
const url = await api.getPreviewUrl(filename, tema)
|
||||
|
||||
setPreviewFileUrl(url)
|
||||
setPreviewModalOpen(true)
|
||||
} catch (error) {
|
||||
console.error('Error obteniendo URL de preview:', error)
|
||||
alert('Error al cargar la vista previa del archivo')
|
||||
} finally {
|
||||
setLoadingPreview(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Manejar descarga desde el modal de preview
|
||||
const handleDownloadFromPreview = async () => {
|
||||
if (previewFileName) {
|
||||
await handleDownloadSingle(previewFileName)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredFiles = files.filter(file =>
|
||||
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
@@ -267,59 +308,75 @@ export function Dashboard() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredFiles.map((file) => (
|
||||
<TableRow key={file.full_path}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedFiles.has(file.name)}
|
||||
onCheckedChange={() => toggleFileSelection(file.name)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{file.name}</TableCell>
|
||||
<TableCell>{formatFileSize(file.size)}</TableCell>
|
||||
<TableCell>{formatDate(file.last_modified)}</TableCell>
|
||||
<TableCell>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md text-sm">
|
||||
{file.tema || 'General'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDownloadSingle(file.name)}
|
||||
disabled={downloading}
|
||||
title="Descargar archivo"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Ver chunks"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Generar preguntas"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteSingle(file.name)}
|
||||
title="Eliminar archivo"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredFiles.map((file) => {
|
||||
const isPDF = file.name.toLowerCase().endsWith('.pdf')
|
||||
|
||||
return (
|
||||
<TableRow key={file.full_path}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedFiles.has(file.name)}
|
||||
onCheckedChange={() => toggleFileSelection(file.name)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{isPDF ? (
|
||||
<button
|
||||
onClick={() => handlePreviewFile(file.name, file.tema || undefined)}
|
||||
className="text-blue-600 hover:text-blue-800 hover:underline text-left transition-colors"
|
||||
disabled={loadingPreview}
|
||||
>
|
||||
{file.name}
|
||||
</button>
|
||||
) : (
|
||||
<span>{file.name}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{formatFileSize(file.size)}</TableCell>
|
||||
<TableCell>{formatDate(file.last_modified)}</TableCell>
|
||||
<TableCell>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md text-sm">
|
||||
{file.tema || 'General'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDownloadSingle(file.name)}
|
||||
disabled={downloading}
|
||||
title="Descargar archivo"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Ver chunks"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Generar preguntas"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteSingle(file.name)}
|
||||
title="Eliminar archivo"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
@@ -340,6 +397,15 @@ export function Dashboard() {
|
||||
loading={deleting}
|
||||
{...getDeleteDialogProps()}
|
||||
/>
|
||||
|
||||
{/* PDF Preview Modal */}
|
||||
<PDFPreviewModal
|
||||
open={previewModalOpen}
|
||||
onOpenChange={setPreviewModalOpen}
|
||||
fileUrl={previewFileUrl}
|
||||
fileName={previewFileName}
|
||||
onDownload={handleDownloadFromPreview}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useState, useEffect } from 'react'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useFileStore } from '@/stores/fileStore'
|
||||
import { api } from '@/services/api'
|
||||
@@ -20,6 +20,12 @@ export function FileUpload({ open, onOpenChange, onSuccess }: FileUploadProps) {
|
||||
const [tema, setTema] = useState(selectedTema || '')
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && selectedTema) {
|
||||
setTema(selectedTema)
|
||||
}
|
||||
}, [open, selectedTema])
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setFiles(prev => [...prev, ...acceptedFiles])
|
||||
}, [])
|
||||
@@ -66,10 +72,9 @@ export function FileUpload({ open, onOpenChange, onSuccess }: FileUploadProps) {
|
||||
setTema('')
|
||||
onOpenChange(false)
|
||||
|
||||
// Aquí deberías recargar la lista de archivos
|
||||
// recargar la lista de archivos
|
||||
onSuccess?.()
|
||||
|
||||
// Puedes agregar una función en el store para esto
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading files:', error)
|
||||
|
||||
155
frontend/src/components/PDFPreviewModal.tsx
Normal file
155
frontend/src/components/PDFPreviewModal.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Download,
|
||||
Loader2,
|
||||
FileText,
|
||||
ExternalLink
|
||||
} from 'lucide-react'
|
||||
|
||||
interface PDFPreviewModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
fileUrl: string | null
|
||||
fileName: string
|
||||
onDownload?: () => void
|
||||
}
|
||||
|
||||
export function PDFPreviewModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
fileUrl,
|
||||
fileName,
|
||||
onDownload
|
||||
}: PDFPreviewModalProps) {
|
||||
// Estado para manejar el loading del iframe
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Efecto para manejar el timeout del loading
|
||||
useEffect(() => {
|
||||
if (open && fileUrl) {
|
||||
setLoading(true)
|
||||
|
||||
// Timeout para ocultar loading automáticamente después de 3 segundos
|
||||
// Algunos iframes no disparan onLoad correctamente
|
||||
const timeout = setTimeout(() => {
|
||||
setLoading(false)
|
||||
}, 3000)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [open, fileUrl])
|
||||
|
||||
// Manejar cuando el iframe termina de cargar
|
||||
const handleIframeLoad = () => {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// Abrir PDF en nueva pestaña
|
||||
const openInNewTab = () => {
|
||||
if (fileUrl) {
|
||||
window.open(fileUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// Reiniciar loading cuando cambia el archivo
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
setLoading(true)
|
||||
}
|
||||
onOpenChange(open)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-6xl max-h-[95vh] h-[95vh] flex flex-col p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-4 border-b">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
{fileName}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Vista previa del documento PDF
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Barra de controles */}
|
||||
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b bg-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={openInNewTab}
|
||||
title="Abrir en nueva pestaña"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
Abrir en pestaña nueva
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Botón de descarga */}
|
||||
{onDownload && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onDownload}
|
||||
title="Descargar archivo"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Descargar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Área de visualización del PDF con iframe */}
|
||||
<div className="flex-1 relative bg-gray-100">
|
||||
{!fileUrl ? (
|
||||
<div className="flex items-center justify-center h-full text-center text-gray-500 p-8">
|
||||
<div>
|
||||
<FileText className="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
||||
<p>No se ha proporcionado un archivo para previsualizar</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Indicador de carga */}
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white z-10">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-12 h-12 animate-spin text-blue-500 mx-auto mb-4" />
|
||||
<p className="text-gray-600">Cargando PDF...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Iframe para mostrar el PDF
|
||||
El navegador maneja toda la visualización, zoom, scroll, etc.
|
||||
Esto muestra el PDF exactamente como se vería si lo abrieras directamente
|
||||
*/}
|
||||
<iframe
|
||||
src={fileUrl}
|
||||
className="w-full h-full border-0"
|
||||
title={`Vista previa de ${fileName}`}
|
||||
onLoad={handleIframeLoad}
|
||||
style={{ minHeight: '600px' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer con información */}
|
||||
<div className="px-6 py-3 border-t bg-gray-50 text-xs text-gray-500 text-center">
|
||||
{fileName}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user