add section tabs

This commit is contained in:
Anibal Angulo
2025-11-06 12:15:00 -06:00
parent 90f32b2508
commit 86e5c955c5
7 changed files with 751 additions and 454 deletions

View File

@@ -1,23 +1,27 @@
import { useEffect, useState } from 'react'
import { useFileStore } from '@/stores/fileStore'
import { api } from '@/services/api'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { useEffect, useState } from "react";
import { useFileStore } from "@/stores/fileStore";
import { api } from "@/services/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
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 { PDFPreviewModal } from './PDFPreviewModal'
import { CollectionVerifier } from './CollectionVerifier'
import { ChunkViewerModal } from './ChunkViewerModal'
import { ChunkingConfigModalLandingAI, type LandingAIConfig } from './ChunkingConfigModalLandingAI'
TableRow,
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { FileUpload } from "./FileUpload";
import { DeleteConfirmDialog } from "./DeleteConfirmDialog";
import { PDFPreviewModal } from "./PDFPreviewModal";
import { CollectionVerifier } from "./CollectionVerifier";
import { ChunkViewerModal } from "./ChunkViewerModal";
import {
ChunkingConfigModalLandingAI,
type LandingAIConfig,
} from "./ChunkingConfigModalLandingAI";
import {
Upload,
Download,
@@ -26,11 +30,11 @@ import {
FileText,
Eye,
MessageSquare,
Scissors
} from 'lucide-react'
Scissors,
} from "lucide-react";
interface DashboardProps {
onProcessingChange?: (isProcessing: boolean) => void
onProcessingChange?: (isProcessing: boolean) => void;
}
export function Dashboard({ onProcessingChange }: DashboardProps = {}) {
@@ -43,451 +47,516 @@ export function Dashboard({ onProcessingChange }: DashboardProps = {}) {
selectedFiles,
toggleFileSelection,
selectAllFiles,
clearSelection
} = useFileStore()
clearSelection,
} = useFileStore();
const [searchTerm, setSearchTerm] = useState('')
const [uploadDialogOpen, setUploadDialogOpen] = useState(false)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [fileToDelete, setFileToDelete] = useState<string | null>(null)
const [deleting, setDeleting] = useState(false)
const [downloading, setDownloading] = useState(false)
const [searchTerm, setSearchTerm] = useState("");
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [fileToDelete, setFileToDelete] = useState<string | null>(null);
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)
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);
// Estados para el modal de chunks
const [chunkViewerOpen, setChunkViewerOpen] = useState(false)
const [chunkFileName, setChunkFileName] = useState('')
const [chunkFileTema, setChunkFileTema] = useState('')
const [chunkViewerOpen, setChunkViewerOpen] = useState(false);
const [chunkFileName, setChunkFileName] = useState("");
const [chunkFileTema, setChunkFileTema] = useState("");
// Estados para chunking
const [chunkingConfigOpen, setChunkingConfigOpen] = useState(false)
const [chunkingFileName, setChunkingFileName] = useState('')
const [chunkingFileTema, setChunkingFileTema] = useState('')
const [chunkingCollectionName, setChunkingCollectionName] = useState('')
const [processing, setProcessing] = useState(false)
const [chunkingConfigOpen, setChunkingConfigOpen] = useState(false);
const [chunkingFileName, setChunkingFileName] = useState("");
const [chunkingFileTema, setChunkingFileTema] = useState("");
const [chunkingCollectionName, setChunkingCollectionName] = useState("");
const [processing, setProcessing] = useState(false);
useEffect(() => {
loadFiles()
}, [selectedTema])
loadFiles();
}, [selectedTema]);
const loadFiles = async () => {
try {
setLoading(true)
const response = await api.getFiles(selectedTema || undefined)
setFiles(response.files)
setLoading(true);
const response = await api.getFiles(selectedTema || undefined);
setFiles(response.files);
} catch (error) {
console.error('Error loading files:', error)
console.error("Error loading files:", error);
} finally {
setLoading(false)
setLoading(false);
}
}
};
const handleUploadSuccess = () => {
loadFiles()
}
loadFiles();
};
// Eliminar archivo individual
const handleDeleteSingle = async (filename: string) => {
setFileToDelete(filename)
setDeleteDialogOpen(true)
}
setFileToDelete(filename);
setDeleteDialogOpen(true);
};
// Eliminar archivos seleccionados
const handleDeleteMultiple = () => {
if (selectedFiles.size === 0) return
setFileToDelete(null)
setDeleteDialogOpen(true)
}
if (selectedFiles.size === 0) return;
setFileToDelete(null);
setDeleteDialogOpen(true);
};
// Confirmar eliminación
const confirmDelete = async () => {
if (!fileToDelete && selectedFiles.size === 0) return
if (!fileToDelete && selectedFiles.size === 0) return;
setDeleting(true)
setDeleting(true);
try {
if (fileToDelete) {
// Eliminar archivo individual
await api.deleteFile(fileToDelete, selectedTema || undefined)
await api.deleteFile(fileToDelete, selectedTema || undefined);
} else {
// Eliminar archivos seleccionados
const filesToDelete = Array.from(selectedFiles)
await api.deleteFiles(filesToDelete, selectedTema || undefined)
clearSelection()
const filesToDelete = Array.from(selectedFiles);
await api.deleteFiles(filesToDelete, selectedTema || undefined);
clearSelection();
}
// Recargar archivos
await loadFiles()
setDeleteDialogOpen(false)
setFileToDelete(null)
await loadFiles();
setDeleteDialogOpen(false);
setFileToDelete(null);
} catch (error) {
console.error('Error deleting files:', error)
console.error("Error deleting files:", error);
} finally {
setDeleting(false)
setDeleting(false);
}
}
};
// Descargar archivo individual
const handleDownloadSingle = async (filename: string) => {
try {
setDownloading(true)
await api.downloadFile(filename, selectedTema || undefined)
setDownloading(true);
await api.downloadFile(filename, selectedTema || undefined);
} catch (error) {
console.error('Error downloading file:', error)
console.error("Error downloading file:", error);
} finally {
setDownloading(false)
setDownloading(false);
}
}
};
// Descargar archivos seleccionados
const handleDownloadMultiple = async () => {
if (selectedFiles.size === 0) return
if (selectedFiles.size === 0) return;
try {
setDownloading(true)
const filesToDownload = Array.from(selectedFiles)
const zipName = selectedTema ? `${selectedTema}_archivos` : 'archivos_seleccionados'
await api.downloadMultipleFiles(filesToDownload, selectedTema || undefined, zipName)
setDownloading(true);
const filesToDownload = Array.from(selectedFiles);
const zipName = selectedTema
? `${selectedTema}_archivos`
: "archivos_seleccionados";
await api.downloadMultipleFiles(
filesToDownload,
selectedTema || undefined,
zipName,
);
} catch (error) {
console.error('Error downloading files:', error)
console.error("Error downloading files:", error);
} finally {
setDownloading(false)
setDownloading(false);
}
}
};
// 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
if (!filename.toLowerCase().endsWith(".pdf")) {
console.log("Solo se pueden previsualizar archivos PDF");
return;
}
try {
setLoadingPreview(true)
setPreviewFileName(filename)
setPreviewFileTema(tema)
setLoadingPreview(true);
setPreviewFileName(filename);
setPreviewFileTema(tema);
// Obtener la URL temporal (SAS) para el archivo
const url = await api.getPreviewUrl(filename, tema)
const url = await api.getPreviewUrl(filename, tema);
setPreviewFileUrl(url)
setPreviewModalOpen(true)
setPreviewFileUrl(url);
setPreviewModalOpen(true);
} catch (error) {
console.error('Error obteniendo URL de preview:', error)
alert('Error al cargar la vista previa del archivo')
console.error("Error obteniendo URL de preview:", error);
alert("Error al cargar la vista previa del archivo");
} finally {
setLoadingPreview(false)
setLoadingPreview(false);
}
}
};
// Manejar descarga desde el modal de preview
const handleDownloadFromPreview = async () => {
if (previewFileName) {
await handleDownloadSingle(previewFileName)
await handleDownloadSingle(previewFileName);
}
}
};
// Abrir modal de chunks
const handleViewChunks = (filename: string, tema: string) => {
if (!tema) {
alert('No hay tema seleccionado. Por favor selecciona un tema primero.')
return
alert("No hay tema seleccionado. Por favor selecciona un tema primero.");
return;
}
setChunkFileName(filename)
setChunkFileTema(tema)
setChunkViewerOpen(true)
}
setChunkFileName(filename);
setChunkFileTema(tema);
setChunkViewerOpen(true);
};
// Handlers para chunking
const handleStartChunking = (filename: string, tema: string) => {
if (!tema) {
alert('No hay tema seleccionado. Por favor selecciona un tema primero.')
return
alert("No hay tema seleccionado. Por favor selecciona un tema primero.");
return;
}
setChunkingFileName(filename)
setChunkingFileTema(tema)
setChunkingCollectionName(tema) // Usar el tema como nombre de colección
setChunkingConfigOpen(true)
}
setChunkingFileName(filename);
setChunkingFileTema(tema);
setChunkingCollectionName(tema); // Usar el tema como nombre de colección
setChunkingConfigOpen(true);
};
const handleProcessWithLandingAI = async (config: LandingAIConfig) => {
setProcessing(true)
onProcessingChange?.(true)
setChunkingConfigOpen(false)
setProcessing(true);
onProcessingChange?.(true);
setChunkingConfigOpen(false);
try {
const result = await api.processWithLandingAI(config)
const result = await api.processWithLandingAI(config);
// Mensaje detallado
let message = `Completado\n\n`
message += `• Modo: ${result.mode === 'quick' ? 'Rápido' : 'Con Extracción'}\n`
message += `• Chunks procesados: ${result.total_chunks}\n`
message += `• Chunks agregados: ${result.chunks_added}\n`
message += `• Colección: ${result.collection_name}\n`
message += `• Tiempo: ${result.processing_time_seconds}s\n`
let message = `Completado\n\n`;
message += `• Modo: ${result.mode === "quick" ? "Rápido" : "Con Extracción"}\n`;
message += `• Chunks procesados: ${result.total_chunks}\n`;
message += `• Chunks agregados: ${result.chunks_added}\n`;
message += `• Colección: ${result.collection_name}\n`;
message += `• Tiempo: ${result.processing_time_seconds}s\n`;
if (result.schema_used) {
message += `• Schema usado: ${result.schema_used}\n`
message += `• Schema usado: ${result.schema_used}\n`;
}
if (result.extracted_data) {
message += `\nDatos extraídos disponibles en metadata`
message += `\nDatos extraídos disponibles en metadata`;
}
alert(message)
alert(message);
// Recargar archivos
loadFiles()
loadFiles();
} catch (error: any) {
console.error('Error processing with LandingAI:', error)
alert(`❌ Error: ${error.message}`)
console.error("Error processing with LandingAI:", error);
alert(`❌ Error: ${error.message}`);
} finally {
setProcessing(false)
onProcessingChange?.(false)
setProcessing(false);
onProcessingChange?.(false);
}
}
};
const filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchTerm.toLowerCase())
)
const filteredFiles = files.filter((file) =>
file.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const totalFiles = files.length;
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
return new Date(dateString).toLocaleDateString("es-ES", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
// Preparar datos para el modal de confirmación
const getDeleteDialogProps = () => {
if (fileToDelete) {
return {
title: 'Eliminar archivo',
title: "Eliminar archivo",
description: `¿Estás seguro de que quieres eliminar "${fileToDelete}"? Esta acción no se puede deshacer.`,
fileList: [fileToDelete]
}
fileList: [fileToDelete],
};
} else {
const filesToDelete = Array.from(selectedFiles)
const filesToDelete = Array.from(selectedFiles);
return {
title: `Eliminar ${filesToDelete.length} archivos`,
description: `¿Estás seguro de que quieres eliminar ${filesToDelete.length} archivo${filesToDelete.length !== 1 ? 's' : ''}? Esta acción no se puede deshacer.`,
fileList: filesToDelete
}
description: `¿Estás seguro de que quieres eliminar ${filesToDelete.length} archivo${filesToDelete.length !== 1 ? "s" : ""}? Esta acción no se puede deshacer.`,
fileList: filesToDelete,
};
}
}
};
return (
<div className="flex flex-col h-full bg-white">
{/* Processing Banner */}
{processing && (
<div className="bg-blue-50 border-b border-blue-200 px-6 py-3">
<div className="flex items-center justify-center gap-3">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
<p className="text-sm font-medium text-blue-900">
Procesando archivo con LandingAI... Por favor no navegues ni realices otras acciones.
</p>
</div>
</div>
)}
{/* Header */}
<div className="border-b border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<div className="border-b border-gray-200 px-6 py-4">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold text-gray-900">
{selectedTema ? `Tema: ${selectedTema}` : 'Todos los archivos'}
{selectedTema
? `Tema actual: ${selectedTema}`
: "Todos los archivos"}
</h2>
<p className="text-sm text-gray-600 mt-1">
{filteredFiles.length} archivo{filteredFiles.length !== 1 ? 's' : ''}
<p className="mt-1 text-sm text-gray-600">
{totalFiles} archivo{totalFiles !== 1 ? "s" : ""}
</p>
</div>
<div className="flex gap-2">
<Button onClick={() => setUploadDialogOpen(true)} disabled={processing}>
<Upload className="w-4 h-4 mr-2" />
Subir archivo
</Button>
</div>
</div>
{/* Search and Actions */}
<div className="flex items-center gap-4">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Buscar archivos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
disabled={processing}
/>
</div>
<Tabs defaultValue="files" className="flex flex-col flex-1">
<div className="border-b border-gray-200 px-6 py-2">
<TabsList className="flex h-10 items-center gap-2 bg-transparent p-0 justify-start">
<TabsTrigger
value="dashboard"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-black data-[state=active]:text-white data-[state=active]:shadow-lg data-[state=active]:ring-0 data-[state=active]:shadow-black/30"
>
Dashboard
</TabsTrigger>
<TabsTrigger
value="files"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-black data-[state=active]:text-white data-[state=active]:shadow-lg data-[state=active]:ring-0 data-[state=active]:shadow-black/30"
>
Files
</TabsTrigger>
<TabsTrigger
value="chat"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-black data-[state=active]:text-white data-[state=active]:shadow-lg data-[state=active]:ring-0 data-[state=active]:shadow-black/30"
>
Chat
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="dashboard" className="mt-0 flex-1">
<div className="p-6 text-sm text-gray-500">
Este panel se llenará con métricas generales próximamente.
</div>
{selectedFiles.size > 0 && (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleDownloadMultiple}
disabled={downloading || processing}
>
<Download className="w-4 h-4 mr-2" />
{downloading ? 'Descargando...' : `Descargar (${selectedFiles.size})`}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDeleteMultiple}
disabled={processing}
>
<Trash2 className="w-4 h-4 mr-2" />
Eliminar ({selectedFiles.size})
</Button>
</TabsContent>
<TabsContent value="files" className="mt-0 flex flex-1 flex-col">
{processing && (
<div className="bg-blue-50 border-b border-blue-200 px-6 py-3">
<div className="flex items-center justify-center gap-3">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
<p className="text-sm font-medium text-blue-900">
Procesando archivo con LandingAI... Por favor no navegues ni
realices otras acciones.
</p>
</div>
</div>
)}
</div>
</div>
{/* Table */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Cargando archivos...</p>
</div>
) : filteredFiles.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64">
<FileText className="w-12 h-12 text-gray-400 mb-4" />
<p className="text-gray-500">
{searchTerm ? 'No se encontraron archivos' : 'No hay archivos en este tema'}
</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedFiles.size === filteredFiles.length && filteredFiles.length > 0}
onCheckedChange={(checked: boolean) => {
if (checked) {
selectAllFiles()
} else {
clearSelection()
}
}}
/>
</TableHead>
<TableHead>Nombre</TableHead>
<TableHead>Tamaño</TableHead>
<TableHead>Fecha</TableHead>
<TableHead>Tema</TableHead>
<TableHead className="w-32">Acciones</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredFiles.map((file) => {
const isPDF = file.name.toLowerCase().endsWith('.pdf')
<div className="border-b border-gray-200 px-6 py-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="relative w-full md:max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Buscar archivos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
disabled={processing}
/>
</div>
return (
<TableRow key={file.full_path}>
<TableCell>
<div className="flex w-full items-center justify-between gap-2 md:w-auto md:justify-end">
{selectedFiles.size > 0 && (
<>
<Button
variant="outline"
size="sm"
onClick={handleDownloadMultiple}
disabled={downloading || processing}
>
<Download className="w-4 h-4 mr-2" />
{downloading
? "Descargando..."
: `Descargar (${selectedFiles.size})`}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDeleteMultiple}
disabled={processing}
>
<Trash2 className="w-4 h-4 mr-2" />
Eliminar ({selectedFiles.size})
</Button>
</>
)}
<Button
onClick={() => setUploadDialogOpen(true)}
disabled={processing}
>
<Upload className="w-4 h-4 mr-2" />
Subir archivo
</Button>
</div>
</div>
</div>
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Cargando archivos...</p>
</div>
) : filteredFiles.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64">
<FileText className="w-12 h-12 text-gray-400 mb-4" />
<p className="text-gray-500">
{searchTerm
? "No se encontraron archivos"
: "No hay archivos en este tema"}
</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedFiles.has(file.name)}
onCheckedChange={() => toggleFileSelection(file.name)}
checked={
selectedFiles.size === filteredFiles.length &&
filteredFiles.length > 0
}
onCheckedChange={(checked: boolean) => {
if (checked) {
selectAllFiles();
} else {
clearSelection();
}
}}
/>
</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="Procesar con chunking"
onClick={() => handleStartChunking(file.name, file.tema)}
>
<Scissors className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
title="Ver chunks"
onClick={() => handleViewChunks(file.name, file.tema)}
>
<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>
</TableHead>
<TableHead>Nombre</TableHead>
<TableHead>Tamaño</TableHead>
<TableHead>Fecha</TableHead>
<TableHead>Tema</TableHead>
<TableHead className="w-32">Acciones</TableHead>
</TableRow>
)
})}
</TableBody>
</Table>
)}
</div>
</TableHeader>
<TableBody>
{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="Procesar con chunking"
onClick={() =>
handleStartChunking(file.name, file.tema)
}
>
<Scissors className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
title="Ver chunks"
onClick={() =>
handleViewChunks(file.name, file.tema)
}
>
<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>
)}
</div>
</TabsContent>
<TabsContent value="chat" className="mt-0 flex-1">
<div className="p-6 text-sm text-gray-500">
El chat estará disponible próximamente.
</div>
</TabsContent>
</Tabs>
{/* Upload Dialog */}
<FileUpload
open={uploadDialogOpen}
<FileUpload
open={uploadDialogOpen}
onOpenChange={setUploadDialogOpen}
onSuccess={handleUploadSuccess}
/>
@@ -514,7 +583,7 @@ export function Dashboard({ onProcessingChange }: DashboardProps = {}) {
<CollectionVerifier
tema={selectedTema}
onVerified={(exists) => {
console.log(`Collection ${selectedTema} exists: ${exists}`)
console.log(`Collection ${selectedTema} exists: ${exists}`);
}}
/>
@@ -536,5 +605,5 @@ export function Dashboard({ onProcessingChange }: DashboardProps = {}) {
onProcess={handleProcessWithLandingAI}
/>
</div>
)
}
);
}