Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d167589c61 | |||
| 29bbb4b43c | |||
| 86fe3a22c6 | |||
| 857b1795fc | |||
| 691b8911c5 | |||
| 073969b9bf | |||
| e173c3097c | |||
| 852564776a |
@@ -43,7 +43,6 @@
|
|||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"use-debounce": "^10.1.0",
|
"use-debounce": "^10.1.0",
|
||||||
"vaul": "^1.1.2",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/devtools-vite": "^0.3.11",
|
"@tanstack/devtools-vite": "^0.3.11",
|
||||||
@@ -1389,8 +1388,6 @@
|
|||||||
|
|
||||||
"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=="],
|
"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": ["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=="],
|
"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=="],
|
||||||
|
|||||||
+1
-2
@@ -55,8 +55,7 @@
|
|||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"use-debounce": "^10.1.0",
|
"use-debounce": "^10.1.0"
|
||||||
"vaul": "^1.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/devtools-vite": "^0.3.11",
|
"@tanstack/devtools-vite": "^0.3.11",
|
||||||
|
|||||||
@@ -249,16 +249,22 @@ export default function AsignaturaDetailPage() {
|
|||||||
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
|
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<GraduationCap className="h-4 w-4 shrink-0" />
|
<GraduationCap className="h-4 w-4 shrink-0" />
|
||||||
<span className="text-blue-100">
|
{/* Eliminamos el max-w y dejamos que el flex-wrap haga su trabajo */}
|
||||||
{asignaturasApi?.planes_estudio?.datos?.nombre || ''}
|
<EditableHeaderField
|
||||||
</span>
|
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>
|
</span>
|
||||||
|
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="text-blue-100">
|
<EditableHeaderField
|
||||||
{asignaturasApi?.planes_estudio?.carreras?.facultades
|
value={
|
||||||
?.nombre || ''}
|
asignaturasApi?.planes_estudio?.carreras?.facultades
|
||||||
</span>
|
?.nombre || ''
|
||||||
|
}
|
||||||
|
onSave={(val) => handleUpdateHeader('facultad_nombre', val)}
|
||||||
|
className="min-w-[10ch] text-blue-100"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Plus, Search, BookOpen, Trash2, Library, Edit3 } from 'lucide-react'
|
import {
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
BookOpen,
|
||||||
|
Trash2,
|
||||||
|
Library,
|
||||||
|
Edit3,
|
||||||
|
Save,
|
||||||
|
} from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -196,6 +204,15 @@ export function BibliographyItem({
|
|||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
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
|
const { data, error } = await supabase
|
||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.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,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,conversation_id,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
|
||||||
)
|
)
|
||||||
.eq('plan_estudio_id', planId)
|
.eq('plan_estudio_id', planId)
|
||||||
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
||||||
@@ -176,31 +176,18 @@ export async function plan_asignaturas_list(
|
|||||||
return data ?? []
|
return data ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_history(
|
export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> {
|
||||||
planId: UUID,
|
|
||||||
page: number = 0,
|
|
||||||
pageSize: number = 4,
|
|
||||||
): Promise<{ data: Array<CambioPlan>; count: number }> {
|
|
||||||
// Cambiamos el retorno
|
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const from = page * pageSize
|
const { data, error } = await supabase
|
||||||
const to = from + pageSize - 1
|
|
||||||
|
|
||||||
const { data, error, count } = await supabase
|
|
||||||
.from('cambios_plan')
|
.from('cambios_plan')
|
||||||
.select(
|
.select(
|
||||||
'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,response_id',
|
'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)
|
.eq('plan_estudio_id', planId)
|
||||||
.order('cambiado_en', { ascending: false })
|
.order('cambiado_en', { ascending: false })
|
||||||
.range(from, to)
|
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error)
|
||||||
return {
|
return data ?? []
|
||||||
data: data ?? [],
|
|
||||||
count: count ?? 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wizard: crear plan manual (Edge Function) */
|
/** Wizard: crear plan manual (Edge Function) */
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
|
|||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.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,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,conversation_id,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||||
planes_estudio(
|
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,
|
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))
|
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
||||||
|
|||||||
@@ -80,17 +80,11 @@ export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlanHistorial(
|
export function usePlanHistorial(planId: UUID | null | undefined) {
|
||||||
planId: UUID | null | undefined,
|
|
||||||
page: number,
|
|
||||||
) {
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: planId
|
queryKey: planId ? qk.planHistorial(planId) : ['planes', 'historial', null],
|
||||||
? [...qk.planHistorial(planId), page]
|
queryFn: () => plans_history(planId as UUID),
|
||||||
: ['planes', 'historial', null, page],
|
|
||||||
queryFn: () => plans_history(planId as UUID, page),
|
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
placeholderData: (previousData) => previousData,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,10 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
History,
|
History,
|
||||||
Calendar,
|
Calendar,
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -60,14 +57,15 @@ const getEventConfig = (tipo: string, campo: string) => {
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const [page, setPage] = useState(0)
|
const { data: rawData, isLoading } = usePlanHistorial(planId)
|
||||||
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 [structure, setStructure] = useState<any>(null)
|
||||||
const { data } = usePlan(planId)
|
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 [selectedEvent, setSelectedEvent] = useState<any>(null)
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
|
||||||
@@ -231,46 +229,6 @@ function RouteComponent() {
|
|||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
|
{/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
/* 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 { createFileRoute, useRouterState } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
|
Sparkles,
|
||||||
Send,
|
Send,
|
||||||
Target,
|
Target,
|
||||||
|
UserCheck,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
FileText,
|
FileText,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Check,
|
Check,
|
||||||
X,
|
X,
|
||||||
MessageSquarePlus,
|
|
||||||
Archive,
|
|
||||||
RotateCcw,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
|
|
||||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||||
|
|
||||||
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Drawer, DrawerContent } from '@/components/ui/drawer'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { usePlan } from '@/data/hooks/usePlans'
|
import { usePlan } from '@/data/hooks/usePlans'
|
||||||
@@ -59,25 +53,22 @@ interface SelectedField {
|
|||||||
value: string
|
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')({
|
export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
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 { data } = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f')
|
||||||
const routerState = useRouterState()
|
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>>([
|
const [messages, setMessages] = useState<Array<any>>([
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -93,62 +84,6 @@ function RouteComponent() {
|
|||||||
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
|
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(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
|
// 1. Transformar datos de la API para el menú de selección
|
||||||
const availableFields = useMemo(() => {
|
const availableFields = useMemo(() => {
|
||||||
@@ -172,11 +107,12 @@ function RouteComponent() {
|
|||||||
f.value === state.campo_edit.label || f.key === state.campo_edit.clave,
|
f.value === state.campo_edit.label || f.key === state.campo_edit.clave,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!field) return
|
if (field && !selectedFields.some((sf) => sf.key === field.key)) {
|
||||||
|
setSelectedFields([field])
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedFields([field])
|
|
||||||
setInput((prev) =>
|
setInput((prev) =>
|
||||||
injectFieldsIntoInput(prev || 'Mejora este campo:', [field]),
|
injectFieldsIntoInput(prev || 'Mejora este campo:', field ? [field] : []),
|
||||||
)
|
)
|
||||||
}, [availableFields])
|
}, [availableFields])
|
||||||
|
|
||||||
@@ -195,21 +131,21 @@ function RouteComponent() {
|
|||||||
input: string,
|
input: string,
|
||||||
fields: Array<SelectedField>,
|
fields: Array<SelectedField>,
|
||||||
) => {
|
) => {
|
||||||
// Quita cualquier bloque previo de campos
|
const baseText = input.replace(/\[[^\]]+]/g, '').trim()
|
||||||
const cleaned = input.replace(/\n?\[Campos:[^\]]*]/g, '').trim()
|
|
||||||
|
|
||||||
if (fields.length === 0) return cleaned
|
const tags = fields.map((f) => `${f.label}`).join(' ')
|
||||||
|
|
||||||
const fieldLabels = fields.map((f) => f.label).join(', ')
|
return `${baseText} ${tags}`.trim()
|
||||||
|
|
||||||
return `${cleaned}\n[Campos: ${fieldLabels}]`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleField = (field: SelectedField) => {
|
const toggleField = (field: SelectedField) => {
|
||||||
setSelectedFields((prev) => {
|
setSelectedFields((prev) => {
|
||||||
const nextFields = prev.find((f) => f.key === field.key)
|
let nextFields
|
||||||
? prev.filter((f) => f.key !== field.key)
|
|
||||||
: [...prev, field]
|
if (prev.find((f) => f.key === field.key)) {
|
||||||
|
nextFields = prev.filter((f) => f.key !== field.key)
|
||||||
|
} else {
|
||||||
|
nextFields = [...prev, field]
|
||||||
|
}
|
||||||
|
|
||||||
setInput((prevInput) =>
|
setInput((prevInput) =>
|
||||||
injectFieldsIntoInput(prevInput || 'Mejora este campo:', nextFields),
|
injectFieldsIntoInput(prevInput || 'Mejora este campo:', nextFields),
|
||||||
@@ -220,7 +156,6 @@ function RouteComponent() {
|
|||||||
|
|
||||||
setShowSuggestions(false)
|
setShowSuggestions(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildPrompt = (userInput: string) => {
|
const buildPrompt = (userInput: string) => {
|
||||||
if (selectedFields.length === 0) return userInput
|
if (selectedFields.length === 0) return userInput
|
||||||
|
|
||||||
@@ -229,11 +164,11 @@ function RouteComponent() {
|
|||||||
.join('\n')
|
.join('\n')
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${userInput || 'Mejora los siguientes campos:'}
|
${userInput || 'Mejora los siguientes campos:'}
|
||||||
|
|
||||||
Campos a analizar:
|
Campos a analizar:
|
||||||
${fieldsText}
|
${fieldsText}
|
||||||
`.trim()
|
`.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSend = async (promptOverride?: string) => {
|
const handleSend = async (promptOverride?: string) => {
|
||||||
@@ -248,157 +183,39 @@ function RouteComponent() {
|
|||||||
content: finalPrompt,
|
content: finalPrompt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// setMessages((prev) => [...prev, userMsg])
|
setMessages((prev) => [...prev, userMsg])
|
||||||
setInput('')
|
setInput('')
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
// setSelectedFields([])
|
|
||||||
setSelectedArchivoIds([])
|
|
||||||
setSelectedRepositorioIds([])
|
|
||||||
setUploadedFiles([])
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const suggestions = selectedFields.map((field) => ({
|
const mockText =
|
||||||
key: field.key,
|
'Sugerencia generada basada en los campos seleccionados...'
|
||||||
label: field.label,
|
|
||||||
newValue: field.value,
|
|
||||||
}))
|
|
||||||
|
|
||||||
setMessages((prev) => [
|
setMessages((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
type: 'improvement-card',
|
content: `He analizado ${selectedFields
|
||||||
content:
|
.map((f) => f.label)
|
||||||
'He analizado los campos seleccionados. Aquí tienes mis sugerencias de mejora:',
|
.join(', ')}. Aquí tienes una propuesta:\n\n${mockText}`,
|
||||||
suggestions: suggestions,
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
setPendingSuggestion({ text: mockText })
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}, 1200)
|
}, 1200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... debajo de tus otros hooks
|
|
||||||
const totalReferencias = useMemo(() => {
|
|
||||||
return (
|
|
||||||
selectedArchivoIds.length +
|
|
||||||
selectedRepositorioIds.length +
|
|
||||||
uploadedFiles.length
|
|
||||||
)
|
|
||||||
}, [selectedArchivoIds, selectedRepositorioIds, uploadedFiles])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
|
<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 */}
|
{/* 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">
|
<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 */}
|
{/* NUEVO: Barra superior de campos seleccionados */}
|
||||||
<div className="shrink-0 border-b bg-white p-3">
|
<div className="shrink-0 border-b bg-white p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-[10px] font-bold text-slate-400 uppercase">
|
<span className="text-[10px] font-bold text-slate-400 uppercase">
|
||||||
Mejorar con IA
|
Mejorar con IA
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -408,29 +225,32 @@ function RouteComponent() {
|
|||||||
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<div
|
<div
|
||||||
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
key={msg.id}
|
||||||
|
className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} items-start gap-3`}
|
||||||
>
|
>
|
||||||
<div
|
<Avatar
|
||||||
className={`rounded-2xl p-3 text-sm whitespace-pre-wrap shadow-sm ${
|
className={`h-8 w-8 shrink-0 border ${msg.role === 'assistant' ? 'bg-teal-50' : 'bg-slate-200'}`}
|
||||||
msg.role === 'user'
|
|
||||||
? 'rounded-tr-none bg-teal-600 text-white'
|
|
||||||
: 'rounded-tl-none border bg-white text-slate-700'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{msg.content}
|
<AvatarFallback className="text-[10px]">
|
||||||
|
{msg.role === 'assistant' ? (
|
||||||
{msg.type === 'improvement-card' && (
|
<Sparkles size={14} className="text-teal-600" />
|
||||||
<ImprovementCard
|
) : (
|
||||||
suggestions={msg.suggestions}
|
<UserCheck size={14} />
|
||||||
onApply={(key, val) => {
|
)}
|
||||||
setSelectedFields((prev) =>
|
</AvatarFallback>
|
||||||
prev.filter((f) => f.key !== key),
|
</Avatar>
|
||||||
)
|
<div
|
||||||
console.log(`Aplicando ${val} al campo ${key}`)
|
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
||||||
// Aquí llamarías a tu función de actualización de datos real
|
>
|
||||||
}}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -465,10 +285,11 @@ function RouteComponent() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
||||||
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
||||||
<div className="shrink-0 border-t bg-white p-4">
|
<div className="shrink-0 border-t bg-white p-4">
|
||||||
<div className="relative mx-auto max-w-4xl">
|
<div className="relative mx-auto max-w-4xl">
|
||||||
{/* MENÚ DE SUGERENCIAS FLOTANTE */}
|
{/* MENÚ DE SUGERENCIAS FLOTANTE (Se mantiene igual) */}
|
||||||
{showSuggestions && (
|
{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="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">
|
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold tracking-wider text-slate-500 uppercase">
|
||||||
@@ -495,7 +316,7 @@ function RouteComponent() {
|
|||||||
|
|
||||||
{/* CONTENEDOR DEL INPUT TRANSFORMADO */}
|
{/* 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">
|
<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 ) */}
|
{/* 1. Visualización de campos dentro del input (Tags) */}
|
||||||
{selectedFields.length > 0 && (
|
{selectedFields.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 px-2 pt-1">
|
<div className="flex flex-wrap gap-2 px-2 pt-1">
|
||||||
{selectedFields.map((field) => (
|
{selectedFields.map((field) => (
|
||||||
@@ -520,13 +341,19 @@ function RouteComponent() {
|
|||||||
<Textarea
|
<Textarea
|
||||||
value={input}
|
value={input}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSend()
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder={
|
placeholder={
|
||||||
selectedFields.length > 0
|
selectedFields.length > 0
|
||||||
? 'Escribe instrucciones adicionales...'
|
? 'Escribe instrucciones adicionales...'
|
||||||
: 'Escribe tu solicitud o ":" para campos...'
|
: '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
|
<Button
|
||||||
onClick={() => handleSend()}
|
onClick={() => handleSend()}
|
||||||
disabled={
|
disabled={
|
||||||
@@ -542,7 +369,8 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* PANEL LATERAL */}
|
|
||||||
|
{/* PANEL LATERAL (PRESETS) - SE MANTIENE COMO LO TENÍAS */}
|
||||||
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
|
<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">
|
<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
|
<Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
|
||||||
@@ -564,104 +392,6 @@ function RouteComponent() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -504,9 +504,6 @@ function MapaCurricularPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<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 >
|
{asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId).length >
|
||||||
0 && (
|
0 && (
|
||||||
<Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
|
<Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
|
||||||
|
|||||||
+6
-131
@@ -88,6 +88,7 @@ export type Database = {
|
|||||||
asignatura_hash: string | null
|
asignatura_hash: string | null
|
||||||
codigo: string | null
|
codigo: string | null
|
||||||
contenido_tematico: Json
|
contenido_tematico: Json
|
||||||
|
conversation_id: string | null
|
||||||
creado_en: string
|
creado_en: string
|
||||||
creado_por: string | null
|
creado_por: string | null
|
||||||
creditos: number
|
creditos: number
|
||||||
@@ -112,6 +113,7 @@ export type Database = {
|
|||||||
asignatura_hash?: string | null
|
asignatura_hash?: string | null
|
||||||
codigo?: string | null
|
codigo?: string | null
|
||||||
contenido_tematico?: Json
|
contenido_tematico?: Json
|
||||||
|
conversation_id?: string | null
|
||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
creditos: number
|
creditos: number
|
||||||
@@ -136,6 +138,7 @@ export type Database = {
|
|||||||
asignatura_hash?: string | null
|
asignatura_hash?: string | null
|
||||||
codigo?: string | null
|
codigo?: string | null
|
||||||
contenido_tematico?: Json
|
contenido_tematico?: Json
|
||||||
|
conversation_id?: string | null
|
||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
creditos?: number
|
creditos?: number
|
||||||
@@ -389,135 +392,6 @@ 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: {
|
estados_plan: {
|
||||||
Row: {
|
Row: {
|
||||||
clave: string
|
clave: string
|
||||||
@@ -801,6 +675,7 @@ export type Database = {
|
|||||||
actualizado_en: string
|
actualizado_en: string
|
||||||
actualizado_por: string | null
|
actualizado_por: string | null
|
||||||
carrera_id: string
|
carrera_id: string
|
||||||
|
conversation_id: string | null
|
||||||
creado_en: string
|
creado_en: string
|
||||||
creado_por: string | null
|
creado_por: string | null
|
||||||
datos: Json
|
datos: Json
|
||||||
@@ -821,6 +696,7 @@ export type Database = {
|
|||||||
actualizado_en?: string
|
actualizado_en?: string
|
||||||
actualizado_por?: string | null
|
actualizado_por?: string | null
|
||||||
carrera_id: string
|
carrera_id: string
|
||||||
|
conversation_id?: string | null
|
||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
datos?: Json
|
datos?: Json
|
||||||
@@ -841,6 +717,7 @@ export type Database = {
|
|||||||
actualizado_en?: string
|
actualizado_en?: string
|
||||||
actualizado_por?: string | null
|
actualizado_por?: string | null
|
||||||
carrera_id?: string
|
carrera_id?: string
|
||||||
|
conversation_id?: string | null
|
||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
datos?: Json
|
datos?: Json
|
||||||
@@ -1211,7 +1088,6 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Enums: {
|
Enums: {
|
||||||
estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando'
|
estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando'
|
||||||
estado_conversacion: 'ACTIVA' | 'ARCHIVANDO' | 'ARCHIVADA' | 'ERROR'
|
|
||||||
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
|
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
|
||||||
fuente_cambio: 'HUMANO' | 'IA'
|
fuente_cambio: 'HUMANO' | 'IA'
|
||||||
nivel_plan_estudio:
|
nivel_plan_estudio:
|
||||||
@@ -1385,7 +1261,6 @@ export const Constants = {
|
|||||||
public: {
|
public: {
|
||||||
Enums: {
|
Enums: {
|
||||||
estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'],
|
estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'],
|
||||||
estado_conversacion: ['ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR'],
|
|
||||||
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
|
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
|
||||||
fuente_cambio: ['HUMANO', 'IA'],
|
fuente_cambio: ['HUMANO', 'IA'],
|
||||||
nivel_plan_estudio: [
|
nivel_plan_estudio: [
|
||||||
|
|||||||
Reference in New Issue
Block a user