diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index a53828a..3c22212 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -18,6 +18,7 @@ import { Route as AuthenticatedFacultadesRouteImport } from './routes/_authentic import { Route as AuthenticatedDashboardRouteImport } from './routes/_authenticated/dashboard' import { Route as AuthenticatedCarrerasRouteImport } from './routes/_authenticated/carreras' import { Route as AuthenticatedAsignaturasRouteImport } from './routes/_authenticated/asignaturas' +import { Route as AuthenticatedArchivos2RouteImport } from './routes/_authenticated/archivos2' import { Route as AuthenticatedArchivosRouteImport } from './routes/_authenticated/archivos' import { Route as AuthenticatedPlanPlanIdRouteImport } from './routes/_authenticated/plan/$planId' import { Route as AuthenticatedFacultadFacultadIdRouteImport } from './routes/_authenticated/facultad/$facultadId' @@ -69,6 +70,11 @@ const AuthenticatedAsignaturasRoute = path: '/asignaturas', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedArchivos2Route = AuthenticatedArchivos2RouteImport.update({ + id: '/archivos2', + path: '/archivos2', + getParentRoute: () => AuthenticatedRoute, +} as any) const AuthenticatedArchivosRoute = AuthenticatedArchivosRouteImport.update({ id: '/archivos', path: '/archivos', @@ -102,6 +108,7 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute '/archivos': typeof AuthenticatedArchivosRoute + '/archivos2': typeof AuthenticatedArchivos2Route '/asignaturas': typeof AuthenticatedAsignaturasRouteWithChildren '/carreras': typeof AuthenticatedCarrerasRoute '/dashboard': typeof AuthenticatedDashboardRoute @@ -117,6 +124,7 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute '/archivos': typeof AuthenticatedArchivosRoute + '/archivos2': typeof AuthenticatedArchivos2Route '/asignaturas': typeof AuthenticatedAsignaturasRouteWithChildren '/carreras': typeof AuthenticatedCarrerasRoute '/dashboard': typeof AuthenticatedDashboardRoute @@ -134,6 +142,7 @@ export interface FileRoutesById { '/_authenticated': typeof AuthenticatedRouteWithChildren '/login': typeof LoginRoute '/_authenticated/archivos': typeof AuthenticatedArchivosRoute + '/_authenticated/archivos2': typeof AuthenticatedArchivos2Route '/_authenticated/asignaturas': typeof AuthenticatedAsignaturasRouteWithChildren '/_authenticated/carreras': typeof AuthenticatedCarrerasRoute '/_authenticated/dashboard': typeof AuthenticatedDashboardRoute @@ -151,6 +160,7 @@ export interface FileRouteTypes { | '/' | '/login' | '/archivos' + | '/archivos2' | '/asignaturas' | '/carreras' | '/dashboard' @@ -166,6 +176,7 @@ export interface FileRouteTypes { | '/' | '/login' | '/archivos' + | '/archivos2' | '/asignaturas' | '/carreras' | '/dashboard' @@ -182,6 +193,7 @@ export interface FileRouteTypes { | '/_authenticated' | '/login' | '/_authenticated/archivos' + | '/_authenticated/archivos2' | '/_authenticated/asignaturas' | '/_authenticated/carreras' | '/_authenticated/dashboard' @@ -265,6 +277,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedAsignaturasRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/archivos2': { + id: '/_authenticated/archivos2' + path: '/archivos2' + fullPath: '/archivos2' + preLoaderRoute: typeof AuthenticatedArchivos2RouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/archivos': { id: '/_authenticated/archivos' path: '/archivos' @@ -319,6 +338,7 @@ const AuthenticatedAsignaturasRouteWithChildren = interface AuthenticatedRouteChildren { AuthenticatedArchivosRoute: typeof AuthenticatedArchivosRoute + AuthenticatedArchivos2Route: typeof AuthenticatedArchivos2Route AuthenticatedAsignaturasRoute: typeof AuthenticatedAsignaturasRouteWithChildren AuthenticatedCarrerasRoute: typeof AuthenticatedCarrerasRoute AuthenticatedDashboardRoute: typeof AuthenticatedDashboardRoute @@ -332,6 +352,7 @@ interface AuthenticatedRouteChildren { const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { AuthenticatedArchivosRoute: AuthenticatedArchivosRoute, + AuthenticatedArchivos2Route: AuthenticatedArchivos2Route, AuthenticatedAsignaturasRoute: AuthenticatedAsignaturasRouteWithChildren, AuthenticatedCarrerasRoute: AuthenticatedCarrerasRoute, AuthenticatedDashboardRoute: AuthenticatedDashboardRoute, diff --git a/src/routes/_authenticated.tsx b/src/routes/_authenticated.tsx index f004f36..4b5b52a 100644 --- a/src/routes/_authenticated.tsx +++ b/src/routes/_authenticated.tsx @@ -47,6 +47,7 @@ const nav = [ { to: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, { to: "/usuarios", label: "Usuarios", icon: Users2Icon }, { to: "/archivos", label: "Archivos de referencia", icon: FileAxis3D }, + { to: "/archivos2", label: "Repositorio de archivos", icon: FileAxis3D }, ] as const function getInitials(name?: string) { diff --git a/src/routes/_authenticated/archivos2.tsx b/src/routes/_authenticated/archivos2.tsx new file mode 100644 index 0000000..a429717 --- /dev/null +++ b/src/routes/_authenticated/archivos2.tsx @@ -0,0 +1,713 @@ +// routes/_authenticated/archivos.tsx +import { createFileRoute, useRouter } from "@tanstack/react-router" +import { useEffect, useMemo, useState } from "react" +import { supabase, useSupabaseAuth } from "@/auth/supabase" +import * as Icons from "lucide-react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Label } from "@/components/ui/label" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Textarea } from "@/components/ui/textarea" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + +type EdgeModule = "files" | "vectorStores" | "vectorStoreFiles" + +interface VectorStore { + id: string + object: "vector_store" + created_at: number + name: string | null + description?: string | null + usage_bytes: number + file_counts: { + in_progress: number + completed: number + failed: number + cancelled: number + total: number + } + status: string + last_active_at?: number | null + metadata?: Record | null +} + +interface VectorStoreFile { + id: string + object: string + created_at: number + vector_store_id: string + status: string + usage_bytes: number + last_error?: { code: string; message: string } | null +} + +interface VectorStoreFileMeta { + id: string + user_id: string | null + vector_store_id: string + openai_file_id: string + label: string | null + tags: string[] | null + created_at: string +} + +type EdgeArgs = { + module: EdgeModule + action: string + params?: Record +} + +async function callFilesAndVectorStoresApi( + args: EdgeArgs, +): Promise { + const { data, error } = await supabase.functions.invoke( + "files-and-vector-stores-api", + { + body: args, + }, + ) + + if (error) { + console.error(error) + throw error + } + + const payload = data ?? {} + if (payload.error) { + const msg = + typeof payload.error === "string" + ? payload.error + : payload.error.message ?? "Error en la función Edge" + throw new Error(msg) + } + + const result = payload.data !== undefined ? payload.data : payload + return result as T +} + +export const Route = createFileRoute("/_authenticated/archivos2")({ + component: RouteComponent, + loader: async () => { + const stores = await callFilesAndVectorStoresApi({ + module: "vectorStores", + action: "list", + params: {}, + }) + return stores ?? [] + }, +}) + +/* ====== UI helpers ====== */ + +function StatusBadge({ status }: { status: string }) { + const label = + status === "completed" + ? "Completado" + : status === "in_progress" + ? "Procesando" + : status + const base = "text-[10px] px-2 py-0.5 rounded-full border" + if (status === "completed") { + return ( + + {label} + + ) + } + if (status === "in_progress") { + return ( + + {label} + + ) + } + return ( + + {label} + + ) +} + +/* ====== Página principal: lista repositorios (Vector Stores) ====== */ + +function RouteComponent() { + const router = useRouter() + const vectorStores = Route.useLoaderData() as VectorStore[] + + const [q, setQ] = useState("") + const [statusFilter, setStatusFilter] = useState<"all" | "completed" | "in_progress">("all") + const [selected, setSelected] = useState(null) + const [dialogOpen, setDialogOpen] = useState(false) + const [createOpen, setCreateOpen] = useState(false) + const [deletingId, setDeletingId] = useState(null) + + const filtered = useMemo(() => { + const term = q.trim().toLowerCase() + return vectorStores.filter((vs) => { + if (statusFilter !== "all" && vs.status !== statusFilter) return false + if (!term) return true + return ( + (vs.name ?? "").toLowerCase().includes(term) || + (vs.description ?? "").toLowerCase().includes(term) + ) + }) + }, [vectorStores, q, statusFilter]) + + function openDetails(vs: VectorStore) { + setSelected(vs) + setDialogOpen(true) + } + + async function handleDelete(id: string) { + if (!confirm("¿Eliminar este repositorio y sus archivos asociados en OpenAI?")) return + setDeletingId(id) + try { + await callFilesAndVectorStoresApi({ + module: "vectorStores", + action: "delete", + params: { id }, + }) + + await supabase + .from("vector_store_files_meta") + .delete() + .eq("vector_store_id", id) + + router.invalidate() + } catch (err: any) { + alert(err?.message ?? "Error al eliminar el repositorio") + } finally { + setDeletingId(null) + } + } + + return ( +
+ + + Repositorios de archivos + +
+
+ + setQ(e.target.value)} + placeholder="Buscar por nombre o descripción…" + className="pl-8" + /> +
+ + + + +
+
+ + + {filtered.length ? ( +
+ {filtered.map((vs) => ( +
+
+
+

+ {vs.name || "(Sin nombre)"} +

+ +
+
+ + Archivos: {vs.file_counts?.completed ?? 0} + + {typeof vs.usage_bytes === "number" && ( + + {(vs.usage_bytes / 1024 / 1024).toFixed(2)} MB + + )} + {vs.last_active_at && ( + + + {new Date(vs.last_active_at * 1000).toLocaleDateString()} + + )} +
+
+ + {vs.description && ( +

+ {vs.description} +

+ )} + +
+ + +
+
+ ))} +
+ ) : ( +
+ No hay repositorios todavía. Crea uno nuevo para empezar 🚀 +
+ )} +
+
+ + router.invalidate()} + /> + + { + setDialogOpen(open) + if (!open) setSelected(null) + }} + onUpdated={() => router.invalidate()} + /> +
+ ) +} + +/* ====== Crear repositorio ====== */ + +function CreateVectorStoreDialog({ + open, + onOpenChange, + onCreated, +}: { + open: boolean + onOpenChange: (open: boolean) => void + onCreated: () => void +}) { + const [name, setName] = useState("") + const [description, setDescription] = useState("") + const [creating, setCreating] = useState(false) + + async function handleCreate() { + if (!name.trim()) { + alert("Escribe un nombre para el repositorio") + return + } + setCreating(true) + try { + await callFilesAndVectorStoresApi({ + module: "vectorStores", + action: "create", + params: { name: name.trim(), description: description.trim() || undefined }, + }) + onOpenChange(false) + setName("") + setDescription("") + onCreated() + } catch (err: any) { + alert(err?.message ?? "Error al crear el repositorio") + } finally { + setCreating(false) + } + } + + return ( + + + + Nuevo repositorio + + Crea un Vector Store para agrupar archivos relacionados. + + + +
+
+ + setName(e.target.value)} + placeholder="Planeación curricular, Entrevistas…" + /> +
+
+ +