diff --git a/backend/app/routers/datarooms.py b/backend/app/routers/datarooms.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 45b6daa..c085b61 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.543.0", @@ -1932,6 +1933,58 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3735414..2b66260 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.543.0", diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index c1eca7f..691f2b6 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -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(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(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(null) - const [previewFileName, setPreviewFileName] = useState('') - const [previewFileTema, setPreviewFileTema] = useState(undefined) - const [loadingPreview, setLoadingPreview] = useState(false) + const [previewModalOpen, setPreviewModalOpen] = useState(false); + const [previewFileUrl, setPreviewFileUrl] = useState(null); + const [previewFileName, setPreviewFileName] = useState(""); + const [previewFileTema, setPreviewFileTema] = useState( + 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 (
- {/* Processing Banner */} - {processing && ( -
-
-
-

- Procesando archivo con LandingAI... Por favor no navegues ni realices otras acciones. -

-
-
- )} - - {/* Header */} -
-
+
+

- {selectedTema ? `Tema: ${selectedTema}` : 'Todos los archivos'} + {selectedTema + ? `Tema actual: ${selectedTema}` + : "Todos los archivos"}

-

- {filteredFiles.length} archivo{filteredFiles.length !== 1 ? 's' : ''} +

+ {totalFiles} archivo{totalFiles !== 1 ? "s" : ""}

- -
- -
- - {/* Search and Actions */} -
-
- - setSearchTerm(e.target.value)} - className="pl-10" - disabled={processing} - /> +
+ +
+ + + Dashboard + + + Files + + + Chat + + +
+ +
+ Este panel se llenará con métricas generales próximamente.
- - {selectedFiles.size > 0 && ( -
- - + + + {processing && ( +
+
+
+

+ Procesando archivo con LandingAI... Por favor no navegues ni + realices otras acciones. +

+
)} -
-
- {/* Table */} -
- {loading ? ( -
-

Cargando archivos...

-
- ) : filteredFiles.length === 0 ? ( -
- -

- {searchTerm ? 'No se encontraron archivos' : 'No hay archivos en este tema'} -

-
- ) : ( - - - - - 0} - onCheckedChange={(checked: boolean) => { - if (checked) { - selectAllFiles() - } else { - clearSelection() - } - }} - /> - - Nombre - Tamaño - Fecha - Tema - Acciones - - - - {filteredFiles.map((file) => { - const isPDF = file.name.toLowerCase().endsWith('.pdf') +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + disabled={processing} + /> +
- return ( - - +
+ {selectedFiles.size > 0 && ( + <> + + + + )} + + +
+
+
+ +
+ {loading ? ( +
+

Cargando archivos...

+
+ ) : filteredFiles.length === 0 ? ( +
+ +

+ {searchTerm + ? "No se encontraron archivos" + : "No hay archivos en este tema"} +

+
+ ) : ( +
+ + + toggleFileSelection(file.name)} + checked={ + selectedFiles.size === filteredFiles.length && + filteredFiles.length > 0 + } + onCheckedChange={(checked: boolean) => { + if (checked) { + selectAllFiles(); + } else { + clearSelection(); + } + }} /> - - - {isPDF ? ( - - ) : ( - {file.name} - )} - - {formatFileSize(file.size)} - {formatDate(file.last_modified)} - - - {file.tema || 'General'} - - - -
- - - - - -
-
+
+ Nombre + Tamaño + Fecha + Tema + Acciones
- ) - })} - -
- )} -
+ + + {filteredFiles.map((file) => { + const isPDF = file.name.toLowerCase().endsWith(".pdf"); + return ( + + + + toggleFileSelection(file.name) + } + /> + + + {isPDF ? ( + + ) : ( + {file.name} + )} + + {formatFileSize(file.size)} + {formatDate(file.last_modified)} + + + {file.tema || "General"} + + + +
+ + + + + +
+
+
+ ); + })} +
+ + )} +
+ + +
+ El chat estará disponible próximamente. +
+
+ {/* Upload Dialog */} - @@ -514,7 +583,7 @@ export function Dashboard({ onProcessingChange }: DashboardProps = {}) { { - console.log(`Collection ${selectedTema} exists: ${exists}`) + console.log(`Collection ${selectedTema} exists: ${exists}`); }} /> @@ -536,5 +605,5 @@ export function Dashboard({ onProcessingChange }: DashboardProps = {}) { onProcess={handleProcessWithLandingAI} />
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index a88360c..85cb547 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -5,11 +5,13 @@ import { Menu } from 'lucide-react' import { Sidebar } from './Sidebar' import { Dashboard } from './Dashboard' import { SchemaManagement } from '@/pages/SchemaManagement' +import { cn } from '@/lib/utils' type View = 'dashboard' | 'schemas' export function Layout() { const [sidebarOpen, setSidebarOpen] = useState(false) + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false) const [currentView, setCurrentView] = useState('dashboard') const [isProcessing, setIsProcessing] = useState(false) @@ -33,19 +35,37 @@ export function Layout() { return (
{/* Desktop Sidebar */} -
- +
+ setIsSidebarCollapsed((prev) => !prev)} + />
{/* Mobile Sidebar */} - - + @@ -66,4 +86,4 @@ export function Layout() {
) -} \ No newline at end of file +} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index dbbb2a2..d0cb243 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,172 +1,296 @@ -import { useEffect, useState } from 'react' -import { useFileStore } from '@/stores/fileStore' -import { api } from '@/services/api' -import { Button } from '@/components/ui/button' -import { FolderIcon, FileText, Trash2, Database } from 'lucide-react' +import { useEffect, useState, type ReactElement } from "react"; +import { useFileStore } from "@/stores/fileStore"; +import { api } from "@/services/api"; +import { Button } from "@/components/ui/button"; +import { + FolderIcon, + FileText, + Trash2, + Database, + ChevronLeft, + ChevronRight, + RefreshCcw, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; interface SidebarProps { - onNavigateToSchemas?: () => void - disabled?: boolean + onNavigateToSchemas?: () => void; + disabled?: boolean; + collapsed?: boolean; + onToggleCollapse?: () => void; } -export function Sidebar({ onNavigateToSchemas, disabled = false }: SidebarProps = {}) { +export function Sidebar({ + onNavigateToSchemas, + disabled = false, + collapsed = false, + onToggleCollapse, +}: SidebarProps = {}) { const { temas, selectedTema, setTemas, setSelectedTema, loading, - setLoading - } = useFileStore() + setLoading, + } = useFileStore(); - const [deletingTema, setDeletingTema] = useState(null) + const [deletingTema, setDeletingTema] = useState(null); + + const renderWithTooltip = (label: string, element: ReactElement) => { + if (!collapsed) { + return element; + } + + return ( + + {element} + + {label} + + + ); + }; useEffect(() => { - loadTemas() - }, []) + loadTemas(); + }, []); const loadTemas = async () => { try { - setLoading(true) - const response = await api.getTemas() - setTemas(response.temas) + setLoading(true); + const response = await api.getTemas(); + setTemas(response.temas); } catch (error) { - console.error('Error loading temas:', error) + console.error("Error loading temas:", error); } finally { - setLoading(false) + setLoading(false); } - } + }; const handleTemaSelect = (tema: string | null) => { - setSelectedTema(tema) - } + setSelectedTema(tema); + }; - const handleDeleteTema = async (tema: string, e: React.MouseEvent) => { - e.stopPropagation() // Evitar que se seleccione el tema al hacer clic en el icono + const handleDeleteTema = async ( + tema: string, + e: React.MouseEvent, + ) => { + e.stopPropagation(); // Evitar que se seleccione el tema al hacer clic en el icono const confirmed = window.confirm( `¿Estás seguro de que deseas eliminar el tema "${tema}"?\n\n` + - `Esto eliminará:\n` + - `• Todos los archivos del tema en Azure Blob Storage\n` + - `• La colección "${tema}" en Qdrant (si existe)\n\n` + - `Esta acción no se puede deshacer.` - ) + `Esto eliminará:\n` + + `• Todos los archivos del tema en Azure Blob Storage\n` + + `• La colección "${tema}" en Qdrant (si existe)\n\n` + + `Esta acción no se puede deshacer.`, + ); - if (!confirmed) return + if (!confirmed) return; try { - setDeletingTema(tema) + setDeletingTema(tema); // 1. Eliminar todos los archivos del tema en Azure Blob Storage - await api.deleteTema(tema) + await api.deleteTema(tema); // 2. Intentar eliminar la colección en Qdrant (si existe) try { - const collectionExists = await api.checkCollectionExists(tema) + const collectionExists = await api.checkCollectionExists(tema); if (collectionExists.exists) { - await api.deleteCollection(tema) - console.log(`Colección "${tema}" eliminada de Qdrant`) + await api.deleteCollection(tema); + console.log(`Colección "${tema}" eliminada de Qdrant`); } } catch (error) { - console.warn(`No se pudo eliminar la colección "${tema}" de Qdrant:`, error) + console.warn( + `No se pudo eliminar la colección "${tema}" de Qdrant:`, + error, + ); // Continuar aunque falle la eliminación de la colección } // 3. Actualizar la lista de temas - await loadTemas() + await loadTemas(); // 4. Si el tema eliminado estaba seleccionado, deseleccionar if (selectedTema === tema) { - setSelectedTema(null) + setSelectedTema(null); } - } catch (error) { - console.error(`Error eliminando tema "${tema}":`, error) - alert(`Error al eliminar el tema: ${error instanceof Error ? error.message : 'Error desconocido'}`) + console.error(`Error eliminando tema "${tema}":`, error); + alert( + `Error al eliminar el tema: ${error instanceof Error ? error.message : "Error desconocido"}`, + ); } finally { - setDeletingTema(null) + setDeletingTema(null); } - } + }; return ( -
- {/* Header */} -
-

- - DoRa Luma -

-
- - {/* Temas List */} -
-
-

Collections

- - {/* Todos los archivos */} - + + {!collapsed &&

DoRa Luma

} +
+ {onToggleCollapse && ( + + )} +
- {/* Lista de temas */} - {loading ? ( -
Cargando...
- ) : ( - temas.map((tema) => ( -
- - + {/* Temas List */} +
+
+

+ {collapsed ? "Coll." : "Collections"} +

+ + {/* Todos los archivos */} + {renderWithTooltip( + "Todos los archivos", + , + )} + + {/* Lista de temas */} + {loading ? ( +
+ {collapsed ? "..." : "Cargando..."}
- )) + ) : ( + temas.map((tema) => ( +
+ {renderWithTooltip( + tema, + , + )} + {!collapsed && ( + + )} +
+ )) + )} +
+
+ + {/* Footer */} +
+ {onNavigateToSchemas && + renderWithTooltip( + "Gestionar Schemas", + , + )} + {renderWithTooltip( + "Actualizar temas", + , )}
- - {/* Footer */} -
- {onNavigateToSchemas && ( - - )} - -
-
- ) -} \ No newline at end of file + + ); +} diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..99ad630 --- /dev/null +++ b/frontend/src/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }