Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c280faef22 | |||
| d6c567195a | |||
| 9c588cfd8f | |||
| 46d8d6142e | |||
| 07d08e1b57 | |||
| ded54c18dd | |||
| 89f264bf5d | |||
| 675c76db74 | |||
| d74807c84e | |||
| 4d0f5815eb | |||
| 2f9e779bce | |||
| 0c57bdfc38 | |||
| 2250a1afd1 | |||
| 9102e756cb | |||
| e788eb788f | |||
| 2ec222694d | |||
| 58d4ee8b6e | |||
| d9a6852f43 | |||
| ba188329dc | |||
| 777be81d2a | |||
| 3afce0de77 | |||
| 4b8ec2c5ab | |||
| 0788002c9b | |||
| c7c631a701 | |||
| 9ba94f2c2c |
@@ -43,6 +43,7 @@
|
||||
"tailwindcss": "^4.0.6",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"use-debounce": "^10.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/devtools-vite": "^0.3.11",
|
||||
@@ -1388,6 +1389,8 @@
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
|
||||
+2
-1
@@ -55,7 +55,8 @@
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"use-debounce": "^10.1.0"
|
||||
"use-debounce": "^10.1.0",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/devtools-vite": "^0.3.11",
|
||||
|
||||
@@ -249,22 +249,16 @@ export default function AsignaturaDetailPage() {
|
||||
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
|
||||
<span className="flex items-center gap-1">
|
||||
<GraduationCap className="h-4 w-4 shrink-0" />
|
||||
{/* Eliminamos el max-w y dejamos que el flex-wrap haga su trabajo */}
|
||||
<EditableHeaderField
|
||||
value={asignaturasApi?.planes_estudio?.datos?.nombre || ''}
|
||||
onSave={(val) => handleUpdateHeader('plan_nombre', val)}
|
||||
className="min-w-[10ch] text-blue-100" // min-w para que sea clickeable si está vacío
|
||||
/>
|
||||
<span className="text-blue-100">
|
||||
{asignaturasApi?.planes_estudio?.datos?.nombre || ''}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-1">
|
||||
<EditableHeaderField
|
||||
value={
|
||||
asignaturasApi?.planes_estudio?.carreras?.facultades
|
||||
?.nombre || ''
|
||||
}
|
||||
onSave={(val) => handleUpdateHeader('facultad_nombre', val)}
|
||||
className="min-w-[10ch] text-blue-100"
|
||||
/>
|
||||
<span className="text-blue-100">
|
||||
{asignaturasApi?.planes_estudio?.carreras?.facultades
|
||||
?.nombre || ''}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
BookOpen,
|
||||
Trash2,
|
||||
Library,
|
||||
Edit3,
|
||||
Save,
|
||||
} from 'lucide-react'
|
||||
import { Plus, Search, BookOpen, Trash2, Library, Edit3 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
@@ -204,15 +196,6 @@ export function BibliographyItem({
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Button
|
||||
onClick={() => onSave(entries)}
|
||||
disabled={isSaving}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />{' '}
|
||||
{isSaving ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
@@ -165,7 +165,7 @@ export async function plan_asignaturas_list(
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.select(
|
||||
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,conversation_id,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
|
||||
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
|
||||
)
|
||||
.eq('plan_estudio_id', planId)
|
||||
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
||||
@@ -176,18 +176,31 @@ export async function plan_asignaturas_list(
|
||||
return data ?? []
|
||||
}
|
||||
|
||||
export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> {
|
||||
export async function plans_history(
|
||||
planId: UUID,
|
||||
page: number = 0,
|
||||
pageSize: number = 4,
|
||||
): Promise<{ data: Array<CambioPlan>; count: number }> {
|
||||
// Cambiamos el retorno
|
||||
const supabase = supabaseBrowser()
|
||||
const { data, error } = await supabase
|
||||
const from = page * pageSize
|
||||
const to = from + pageSize - 1
|
||||
|
||||
const { data, error, count } = await supabase
|
||||
.from('cambios_plan')
|
||||
.select(
|
||||
'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,response_id',
|
||||
{ count: 'exact' }, // <--- Pedimos el conteo exacto
|
||||
)
|
||||
.eq('plan_estudio_id', planId)
|
||||
.order('cambiado_en', { ascending: false })
|
||||
.range(from, to)
|
||||
|
||||
throwIfError(error)
|
||||
return data ?? []
|
||||
return {
|
||||
data: data ?? [],
|
||||
count: count ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
/** Wizard: crear plan manual (Edge Function) */
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
|
||||
.from('asignaturas')
|
||||
.select(
|
||||
`
|
||||
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,conversation_id,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||
planes_estudio(
|
||||
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
||||
|
||||
@@ -80,11 +80,17 @@ export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
||||
})
|
||||
}
|
||||
|
||||
export function usePlanHistorial(planId: UUID | null | undefined) {
|
||||
export function usePlanHistorial(
|
||||
planId: UUID | null | undefined,
|
||||
page: number,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: planId ? qk.planHistorial(planId) : ['planes', 'historial', null],
|
||||
queryFn: () => plans_history(planId as UUID),
|
||||
queryKey: planId
|
||||
? [...qk.planHistorial(planId), page]
|
||||
: ['planes', 'historial', null, page],
|
||||
queryFn: () => plans_history(planId as UUID, page),
|
||||
enabled: Boolean(planId),
|
||||
placeholderData: (previousData) => previousData,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,13 @@ import {
|
||||
Eye,
|
||||
History,
|
||||
Calendar,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -57,15 +60,14 @@ const getEventConfig = (tipo: string, campo: string) => {
|
||||
|
||||
function RouteComponent() {
|
||||
const { planId } = Route.useParams()
|
||||
const { data: rawData, isLoading } = usePlanHistorial(planId)
|
||||
const [page, setPage] = useState(0)
|
||||
const pageSize = 4
|
||||
const { data: response, isLoading } = usePlanHistorial(planId, page)
|
||||
const rawData = response?.data ?? []
|
||||
const totalRecords = response?.count ?? 0
|
||||
const totalPages = Math.ceil(totalRecords / pageSize)
|
||||
const [structure, setStructure] = useState<any>(null)
|
||||
const { data } = usePlan(planId)
|
||||
console.log('analizando estructura')
|
||||
|
||||
console.log(data?.estructuras_plan?.definicion?.properties)
|
||||
// console.log(structure)
|
||||
|
||||
// ESTADOS PARA EL MODAL
|
||||
const [selectedEvent, setSelectedEvent] = useState<any>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
@@ -229,6 +231,46 @@ function RouteComponent() {
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{historyEvents.length > 0 && (
|
||||
<div className="mt-10 ml-20 flex items-center justify-between border-t pt-4">
|
||||
<p className="text-xs text-slate-500">
|
||||
Mostrando {rawData.length} de {totalRecords} cambios
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPage((p) => Math.max(0, p - 1))
|
||||
window.scrollTo(0, 0) // Opcional: volver arriba
|
||||
}}
|
||||
disabled={page === 0 || isLoading}
|
||||
>
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
<span className="text-sm font-medium text-slate-700">
|
||||
Página {page + 1} de {totalPages || 1}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPage((p) => p + 1)
|
||||
window.scrollTo(0, 0)
|
||||
}}
|
||||
// Ahora se deshabilita si llegamos a la última página real
|
||||
disabled={page + 1 >= totalPages || isLoading}
|
||||
>
|
||||
Siguiente
|
||||
<ChevronRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import { createFileRoute, useRouterState } from '@tanstack/react-router'
|
||||
import {
|
||||
Sparkles,
|
||||
Send,
|
||||
Target,
|
||||
UserCheck,
|
||||
Lightbulb,
|
||||
FileText,
|
||||
GraduationCap,
|
||||
BookOpen,
|
||||
Check,
|
||||
X,
|
||||
MessageSquarePlus,
|
||||
Archive,
|
||||
RotateCcw,
|
||||
} from 'lucide-react'
|
||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||
|
||||
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Drawer, DrawerContent } from '@/components/ui/drawer'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { usePlan } from '@/data/hooks/usePlans'
|
||||
@@ -53,22 +59,25 @@ interface SelectedField {
|
||||
value: string
|
||||
}
|
||||
|
||||
const formatLabel = (key: string) => {
|
||||
const result = key.replace(/_/g, ' ')
|
||||
return result.charAt(0).toUpperCase() + result.slice(1)
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const { planId } = Route.useParams()
|
||||
// Usamos el ID dinámico del plan o el hardcoded según tu necesidad
|
||||
|
||||
const { data } = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f')
|
||||
const routerState = useRouterState()
|
||||
const [openIA, setOpenIA] = useState(false)
|
||||
// archivos
|
||||
const [selectedArchivoIds, setSelectedArchivoIds] = useState<Array<string>>(
|
||||
[],
|
||||
)
|
||||
const [selectedRepositorioIds, setSelectedRepositorioIds] = useState<
|
||||
Array<string>
|
||||
>([])
|
||||
const [uploadedFiles, setUploadedFiles] = useState<Array<UploadedFile>>([])
|
||||
|
||||
// ESTADOS PRINCIPALES
|
||||
const [messages, setMessages] = useState<Array<any>>([
|
||||
{
|
||||
id: '1',
|
||||
@@ -84,6 +93,62 @@ function RouteComponent() {
|
||||
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [activeChatId, setActiveChatId] = useState('1')
|
||||
const [chatHistory, setChatHistory] = useState([
|
||||
{ id: '1', title: 'Chat inicial' },
|
||||
])
|
||||
const [showArchived, setShowArchived] = useState(false)
|
||||
const [archivedHistory, setArchivedHistory] = useState<Array<any>>([])
|
||||
const [allMessages, setAllMessages] = useState<{ [key: string]: Array<any> }>(
|
||||
{
|
||||
'1': [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'assistant',
|
||||
content: '¡Hola! Soy tu asistente de IA en este chat inicial.',
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
const createNewChat = () => {
|
||||
const newId = Date.now().toString()
|
||||
const newChat = { id: newId, title: `Nuevo chat ${chatHistory.length + 1}` }
|
||||
|
||||
setChatHistory([newChat, ...chatHistory])
|
||||
setAllMessages({
|
||||
...allMessages,
|
||||
[newId]: [
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
content: '¡Nuevo chat creado! ¿En qué puedo ayudarte?',
|
||||
},
|
||||
],
|
||||
})
|
||||
setActiveChatId(newId)
|
||||
}
|
||||
|
||||
const archiveChat = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation()
|
||||
|
||||
const chatToArchive = chatHistory.find((chat) => chat.id === id)
|
||||
if (chatToArchive) {
|
||||
setArchivedHistory([chatToArchive, ...archivedHistory])
|
||||
const newHistory = chatHistory.filter((chat) => chat.id !== id)
|
||||
setChatHistory(newHistory)
|
||||
if (activeChatId === id && newHistory.length > 0) {
|
||||
setActiveChatId(newHistory[0].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
const unarchiveChat = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation()
|
||||
const chatToRestore = archivedHistory.find((chat) => chat.id === id)
|
||||
if (chatToRestore) {
|
||||
setChatHistory([chatToRestore, ...chatHistory])
|
||||
setArchivedHistory(archivedHistory.filter((chat) => chat.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Transformar datos de la API para el menú de selección
|
||||
const availableFields = useMemo(() => {
|
||||
@@ -107,12 +172,11 @@ function RouteComponent() {
|
||||
f.value === state.campo_edit.label || f.key === state.campo_edit.clave,
|
||||
)
|
||||
|
||||
if (field && !selectedFields.some((sf) => sf.key === field.key)) {
|
||||
setSelectedFields([field])
|
||||
}
|
||||
if (!field) return
|
||||
|
||||
setSelectedFields([field])
|
||||
setInput((prev) =>
|
||||
injectFieldsIntoInput(prev || 'Mejora este campo:', field ? [field] : []),
|
||||
injectFieldsIntoInput(prev || 'Mejora este campo:', [field]),
|
||||
)
|
||||
}, [availableFields])
|
||||
|
||||
@@ -131,21 +195,21 @@ function RouteComponent() {
|
||||
input: string,
|
||||
fields: Array<SelectedField>,
|
||||
) => {
|
||||
const baseText = input.replace(/\[[^\]]+]/g, '').trim()
|
||||
// Quita cualquier bloque previo de campos
|
||||
const cleaned = input.replace(/\n?\[Campos:[^\]]*]/g, '').trim()
|
||||
|
||||
const tags = fields.map((f) => `${f.label}`).join(' ')
|
||||
if (fields.length === 0) return cleaned
|
||||
|
||||
return `${baseText} ${tags}`.trim()
|
||||
const fieldLabels = fields.map((f) => f.label).join(', ')
|
||||
|
||||
return `${cleaned}\n[Campos: ${fieldLabels}]`
|
||||
}
|
||||
|
||||
const toggleField = (field: SelectedField) => {
|
||||
setSelectedFields((prev) => {
|
||||
let nextFields
|
||||
|
||||
if (prev.find((f) => f.key === field.key)) {
|
||||
nextFields = prev.filter((f) => f.key !== field.key)
|
||||
} else {
|
||||
nextFields = [...prev, field]
|
||||
}
|
||||
const nextFields = prev.find((f) => f.key === field.key)
|
||||
? prev.filter((f) => f.key !== field.key)
|
||||
: [...prev, field]
|
||||
|
||||
setInput((prevInput) =>
|
||||
injectFieldsIntoInput(prevInput || 'Mejora este campo:', nextFields),
|
||||
@@ -156,6 +220,7 @@ function RouteComponent() {
|
||||
|
||||
setShowSuggestions(false)
|
||||
}
|
||||
|
||||
const buildPrompt = (userInput: string) => {
|
||||
if (selectedFields.length === 0) return userInput
|
||||
|
||||
@@ -164,11 +229,11 @@ function RouteComponent() {
|
||||
.join('\n')
|
||||
|
||||
return `
|
||||
${userInput || 'Mejora los siguientes campos:'}
|
||||
${userInput || 'Mejora los siguientes campos:'}
|
||||
|
||||
Campos a analizar:
|
||||
${fieldsText}
|
||||
`.trim()
|
||||
Campos a analizar:
|
||||
${fieldsText}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
const handleSend = async (promptOverride?: string) => {
|
||||
@@ -183,39 +248,157 @@ ${fieldsText}
|
||||
content: finalPrompt,
|
||||
}
|
||||
|
||||
setMessages((prev) => [...prev, userMsg])
|
||||
// setMessages((prev) => [...prev, userMsg])
|
||||
setInput('')
|
||||
setIsLoading(true)
|
||||
// setSelectedFields([])
|
||||
setSelectedArchivoIds([])
|
||||
setSelectedRepositorioIds([])
|
||||
setUploadedFiles([])
|
||||
|
||||
setTimeout(() => {
|
||||
const mockText =
|
||||
'Sugerencia generada basada en los campos seleccionados...'
|
||||
const suggestions = selectedFields.map((field) => ({
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
newValue: field.value,
|
||||
}))
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
role: 'assistant',
|
||||
content: `He analizado ${selectedFields
|
||||
.map((f) => f.label)
|
||||
.join(', ')}. Aquí tienes una propuesta:\n\n${mockText}`,
|
||||
type: 'improvement-card',
|
||||
content:
|
||||
'He analizado los campos seleccionados. Aquí tienes mis sugerencias de mejora:',
|
||||
suggestions: suggestions,
|
||||
},
|
||||
])
|
||||
|
||||
setPendingSuggestion({ text: mockText })
|
||||
setIsLoading(false)
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
// ... debajo de tus otros hooks
|
||||
const totalReferencias = useMemo(() => {
|
||||
return (
|
||||
selectedArchivoIds.length +
|
||||
selectedRepositorioIds.length +
|
||||
uploadedFiles.length
|
||||
)
|
||||
}, [selectedArchivoIds, selectedRepositorioIds, uploadedFiles])
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
|
||||
{/* --- PANEL IZQUIERDO: HISTORIAL --- */}
|
||||
<div className="flex w-64 flex-col border-r pr-4">
|
||||
<div className="mb-4">
|
||||
<div className="mb-4 flex items-center justify-between px-2">
|
||||
<h2 className="text-xs font-bold tracking-wider text-slate-500 uppercase">
|
||||
Chats
|
||||
</h2>
|
||||
{/* Botón de toggle archivados movido aquí arriba */}
|
||||
<button
|
||||
onClick={() => setShowArchived(!showArchived)}
|
||||
className={`rounded-md p-1.5 transition-colors ${
|
||||
showArchived
|
||||
? 'bg-teal-50 text-teal-600'
|
||||
: 'text-slate-400 hover:bg-slate-100'
|
||||
}`}
|
||||
title={showArchived ? 'Ver chats activos' : 'Ver archivados'}
|
||||
>
|
||||
<Archive size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={createNewChat}
|
||||
variant="outline"
|
||||
className="mb-4 w-full justify-start gap-2 border-slate-200 hover:bg-teal-50 hover:text-teal-700"
|
||||
>
|
||||
<MessageSquarePlus size={18} /> Nuevo chat
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-1">
|
||||
{/* Lógica de renderizado condicional */}
|
||||
{!showArchived ? (
|
||||
// LISTA DE CHATS ACTIVOS
|
||||
chatHistory.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
onClick={() => setActiveChatId(chat.id)}
|
||||
className={`group relative flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-3 text-sm transition-colors ${
|
||||
activeChatId === chat.id
|
||||
? 'bg-slate-100 font-medium text-slate-900'
|
||||
: 'text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<FileText size={16} className="shrink-0 opacity-40" />
|
||||
<span className="truncate pr-8">{chat.title}</span>
|
||||
<button
|
||||
onClick={(e) => archiveChat(e, chat.id)}
|
||||
className="absolute right-2 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-amber-600"
|
||||
title="Archivar"
|
||||
>
|
||||
<Archive size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
// LISTA DE CHATS ARCHIVADOS
|
||||
<div className="animate-in fade-in slide-in-from-left-2">
|
||||
<p className="mb-2 px-2 text-[10px] font-bold text-slate-400 uppercase">
|
||||
Archivados
|
||||
</p>
|
||||
{archivedHistory.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
className="group relative mb-1 flex w-full items-center gap-3 rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400"
|
||||
>
|
||||
<Archive size={14} className="shrink-0 opacity-30" />
|
||||
<span className="truncate pr-8">{chat.title}</span>
|
||||
<button
|
||||
onClick={(e) => unarchiveChat(e, chat.id)}
|
||||
className="absolute right-2 p-1 opacity-0 group-hover:opacity-100 hover:text-teal-600"
|
||||
title="Desarchivar"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{archivedHistory.length === 0 && (
|
||||
<div className="px-2 py-4 text-center">
|
||||
<p className="text-xs text-slate-400 italic">
|
||||
No hay archivados
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
{/* PANEL DE CHAT PRINCIPAL */}
|
||||
<div className="relative flex min-w-0 flex-[3] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm">
|
||||
{/* NUEVO: Barra superior de campos seleccionados */}
|
||||
<div className="shrink-0 border-b bg-white p-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase">
|
||||
Mejorar con IA
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setOpenIA(true)}
|
||||
className="flex items-center gap-2 rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium transition hover:bg-slate-200"
|
||||
>
|
||||
<Archive size={14} className="text-slate-500" />
|
||||
Referencias
|
||||
{totalReferencias > 0 && (
|
||||
<span className="flex h-4 min-w-[16px] items-center justify-center rounded-full bg-teal-600 px-1 text-[10px] text-white">
|
||||
{totalReferencias}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -225,32 +408,29 @@ ${fieldsText}
|
||||
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} items-start gap-3`}
|
||||
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
||||
>
|
||||
<Avatar
|
||||
className={`h-8 w-8 shrink-0 border ${msg.role === 'assistant' ? 'bg-teal-50' : 'bg-slate-200'}`}
|
||||
>
|
||||
<AvatarFallback className="text-[10px]">
|
||||
{msg.role === 'assistant' ? (
|
||||
<Sparkles size={14} className="text-teal-600" />
|
||||
) : (
|
||||
<UserCheck size={14} />
|
||||
)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div
|
||||
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
||||
className={`rounded-2xl p-3 text-sm whitespace-pre-wrap shadow-sm ${
|
||||
msg.role === 'user'
|
||||
? 'rounded-tr-none bg-teal-600 text-white'
|
||||
: 'rounded-tl-none border bg-white text-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-2xl p-3 text-sm whitespace-pre-wrap shadow-sm ${
|
||||
msg.role === 'user'
|
||||
? 'rounded-tr-none bg-teal-600 text-white'
|
||||
: 'rounded-tl-none border bg-white text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
{msg.content}
|
||||
|
||||
{msg.type === 'improvement-card' && (
|
||||
<ImprovementCard
|
||||
suggestions={msg.suggestions}
|
||||
onApply={(key, val) => {
|
||||
setSelectedFields((prev) =>
|
||||
prev.filter((f) => f.key !== key),
|
||||
)
|
||||
console.log(`Aplicando ${val} al campo ${key}`)
|
||||
// Aquí llamarías a tu función de actualización de datos real
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -285,11 +465,10 @@ ${fieldsText}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
||||
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
||||
<div className="shrink-0 border-t bg-white p-4">
|
||||
<div className="relative mx-auto max-w-4xl">
|
||||
{/* MENÚ DE SUGERENCIAS FLOTANTE (Se mantiene igual) */}
|
||||
{/* MENÚ DE SUGERENCIAS FLOTANTE */}
|
||||
{showSuggestions && (
|
||||
<div className="animate-in slide-in-from-bottom-2 absolute bottom-full z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl">
|
||||
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold tracking-wider text-slate-500 uppercase">
|
||||
@@ -316,7 +495,7 @@ ${fieldsText}
|
||||
|
||||
{/* CONTENEDOR DEL INPUT TRANSFORMADO */}
|
||||
<div className="flex flex-col gap-2 rounded-xl border bg-slate-50 p-2 transition-all focus-within:bg-white focus-within:ring-1 focus-within:ring-teal-500">
|
||||
{/* 1. Visualización de campos dentro del input (Tags) */}
|
||||
{/* 1. Visualización de campos dentro del input ) */}
|
||||
{selectedFields.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 px-2 pt-1">
|
||||
{selectedFields.map((field) => (
|
||||
@@ -341,19 +520,13 @@ ${fieldsText}
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
selectedFields.length > 0
|
||||
? 'Escribe instrucciones adicionales...'
|
||||
: 'Escribe tu solicitud o ":" para campos...'
|
||||
}
|
||||
className="max-h-[120px] min-h-[40px] flex-1 resize-none border-none bg-transparent py-2 text-sm shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => handleSend()}
|
||||
disabled={
|
||||
@@ -369,8 +542,7 @@ ${fieldsText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PANEL LATERAL (PRESETS) - SE MANTIENE COMO LO TENÍAS */}
|
||||
{/* PANEL LATERAL */}
|
||||
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
|
||||
<h4 className="flex items-center gap-2 text-left text-sm font-bold text-slate-800">
|
||||
<Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
|
||||
@@ -392,6 +564,104 @@ ${fieldsText}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Drawer open={openIA} onOpenChange={setOpenIA}>
|
||||
<DrawerContent className="fixed inset-0 h-screen w-screen max-w-none rounded-none">
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<h2 className="text-sm font-semibold">Referencias para la IA</h2>
|
||||
|
||||
<button
|
||||
onClick={() => setOpenIA(false)}
|
||||
className="text-muted-foreground hover:text-foreground text-sm"
|
||||
>
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100vh-60px)] overflow-y-auto p-6">
|
||||
<ReferenciasParaIA
|
||||
selectedArchivoIds={selectedArchivoIds}
|
||||
selectedRepositorioIds={selectedRepositorioIds}
|
||||
uploadedFiles={uploadedFiles}
|
||||
onToggleArchivo={(id, checked) => {
|
||||
setSelectedArchivoIds((prev) =>
|
||||
checked ? [...prev, id] : prev.filter((a) => a !== id),
|
||||
)
|
||||
}}
|
||||
onToggleRepositorio={(id, checked) => {
|
||||
setSelectedRepositorioIds((prev) =>
|
||||
checked ? [...prev, id] : prev.filter((r) => r !== id),
|
||||
)
|
||||
}}
|
||||
onFilesChange={(files) => {
|
||||
setUploadedFiles(files)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ImprovementCard = ({
|
||||
suggestions,
|
||||
onApply,
|
||||
}: {
|
||||
suggestions: Array<any>
|
||||
onApply: (key: string, value: string) => void
|
||||
}) => {
|
||||
// Estado para rastrear qué campos han sido aplicados
|
||||
const [appliedFields, setAppliedFields] = useState<Array<string>>([])
|
||||
|
||||
const handleApply = (key: string, value: string) => {
|
||||
onApply(key, value)
|
||||
setAppliedFields((prev) => [...prev, key])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex w-full flex-col gap-4">
|
||||
{suggestions.map((sug) => {
|
||||
const isApplied = appliedFields.includes(sug.key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={sug.key}
|
||||
className="rounded-2xl border border-slate-100 bg-white p-5 shadow-sm"
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-slate-900">{sug.label}</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleApply(sug.key, sug.newValue)}
|
||||
disabled={isApplied}
|
||||
className={`h-8 rounded-full px-4 text-xs transition-all ${
|
||||
isApplied
|
||||
? 'cursor-not-allowed bg-slate-100 text-slate-400'
|
||||
: 'bg-[#00a189] text-white hover:bg-[#008f7a]'
|
||||
}`}
|
||||
>
|
||||
{isApplied ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Check size={12} /> Aplicado
|
||||
</span>
|
||||
) : (
|
||||
'Aplicar mejora'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`rounded-xl border p-3 text-sm transition-colors duration-300 ${
|
||||
isApplied
|
||||
? 'border-[#ccfbf1] bg-[#f0fdfa] text-slate-700'
|
||||
: 'border-slate-200 bg-slate-50 text-slate-500'
|
||||
}`}
|
||||
>
|
||||
{sug.newValue}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -504,6 +504,9 @@ function MapaCurricularPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button className="bg-teal-700 text-white hover:bg-teal-800">
|
||||
<Plus size={16} className="mr-2" /> Exportar{' '}
|
||||
</Button>
|
||||
{asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId).length >
|
||||
0 && (
|
||||
<Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
|
||||
|
||||
+131
-6
@@ -88,7 +88,6 @@ export type Database = {
|
||||
asignatura_hash: string | null
|
||||
codigo: string | null
|
||||
contenido_tematico: Json
|
||||
conversation_id: string | null
|
||||
creado_en: string
|
||||
creado_por: string | null
|
||||
creditos: number
|
||||
@@ -113,7 +112,6 @@ export type Database = {
|
||||
asignatura_hash?: string | null
|
||||
codigo?: string | null
|
||||
contenido_tematico?: Json
|
||||
conversation_id?: string | null
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
creditos: number
|
||||
@@ -138,7 +136,6 @@ export type Database = {
|
||||
asignatura_hash?: string | null
|
||||
codigo?: string | null
|
||||
contenido_tematico?: Json
|
||||
conversation_id?: string | null
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
creditos?: number
|
||||
@@ -392,6 +389,135 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
conversaciones_asignatura: {
|
||||
Row: {
|
||||
archivado_en: string | null
|
||||
archivado_por: string | null
|
||||
asignatura_id: string
|
||||
conversacion_json: Json
|
||||
creado_en: string
|
||||
creado_por: string | null
|
||||
estado: Database['public']['Enums']['estado_conversacion']
|
||||
id: string
|
||||
intento_archivado: number
|
||||
openai_conversation_id: string
|
||||
}
|
||||
Insert: {
|
||||
archivado_en?: string | null
|
||||
archivado_por?: string | null
|
||||
asignatura_id: string
|
||||
conversacion_json?: Json
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
estado?: Database['public']['Enums']['estado_conversacion']
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
openai_conversation_id: string
|
||||
}
|
||||
Update: {
|
||||
archivado_en?: string | null
|
||||
archivado_por?: string | null
|
||||
asignatura_id?: string
|
||||
conversacion_json?: Json
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
estado?: Database['public']['Enums']['estado_conversacion']
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
openai_conversation_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'conversaciones_asignatura_archivado_por_fkey'
|
||||
columns: ['archivado_por']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'usuarios_app'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_asignatura_asignatura_id_fkey'
|
||||
columns: ['asignatura_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'asignaturas'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_asignatura_creado_por_fkey'
|
||||
columns: ['creado_por']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'usuarios_app'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
conversaciones_plan: {
|
||||
Row: {
|
||||
archivado_en: string | null
|
||||
archivado_por: string | null
|
||||
conversacion_json: Json
|
||||
creado_en: string
|
||||
creado_por: string | null
|
||||
estado: Database['public']['Enums']['estado_conversacion']
|
||||
id: string
|
||||
intento_archivado: number
|
||||
openai_conversation_id: string
|
||||
plan_estudio_id: string
|
||||
}
|
||||
Insert: {
|
||||
archivado_en?: string | null
|
||||
archivado_por?: string | null
|
||||
conversacion_json?: Json
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
estado?: Database['public']['Enums']['estado_conversacion']
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
openai_conversation_id: string
|
||||
plan_estudio_id: string
|
||||
}
|
||||
Update: {
|
||||
archivado_en?: string | null
|
||||
archivado_por?: string | null
|
||||
conversacion_json?: Json
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
estado?: Database['public']['Enums']['estado_conversacion']
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
openai_conversation_id?: string
|
||||
plan_estudio_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'conversaciones_plan_archivado_por_fkey'
|
||||
columns: ['archivado_por']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'usuarios_app'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_plan_creado_por_fkey'
|
||||
columns: ['creado_por']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'usuarios_app'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_plan_plan_estudio_id_fkey'
|
||||
columns: ['plan_estudio_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'planes_estudio'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_plan_plan_estudio_id_fkey'
|
||||
columns: ['plan_estudio_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'plantilla_plan'
|
||||
referencedColumns: ['plan_estudio_id']
|
||||
},
|
||||
]
|
||||
}
|
||||
estados_plan: {
|
||||
Row: {
|
||||
clave: string
|
||||
@@ -675,7 +801,6 @@ export type Database = {
|
||||
actualizado_en: string
|
||||
actualizado_por: string | null
|
||||
carrera_id: string
|
||||
conversation_id: string | null
|
||||
creado_en: string
|
||||
creado_por: string | null
|
||||
datos: Json
|
||||
@@ -696,7 +821,6 @@ export type Database = {
|
||||
actualizado_en?: string
|
||||
actualizado_por?: string | null
|
||||
carrera_id: string
|
||||
conversation_id?: string | null
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
datos?: Json
|
||||
@@ -717,7 +841,6 @@ export type Database = {
|
||||
actualizado_en?: string
|
||||
actualizado_por?: string | null
|
||||
carrera_id?: string
|
||||
conversation_id?: string | null
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
datos?: Json
|
||||
@@ -1088,6 +1211,7 @@ export type Database = {
|
||||
}
|
||||
Enums: {
|
||||
estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando'
|
||||
estado_conversacion: 'ACTIVA' | 'ARCHIVANDO' | 'ARCHIVADA' | 'ERROR'
|
||||
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
|
||||
fuente_cambio: 'HUMANO' | 'IA'
|
||||
nivel_plan_estudio:
|
||||
@@ -1261,6 +1385,7 @@ export const Constants = {
|
||||
public: {
|
||||
Enums: {
|
||||
estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'],
|
||||
estado_conversacion: ['ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR'],
|
||||
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
|
||||
fuente_cambio: ['HUMANO', 'IA'],
|
||||
nivel_plan_estudio: [
|
||||
|
||||
Reference in New Issue
Block a user