finalización del merge de main a la rama issue/129...
This commit is contained in:
@@ -1,18 +1,8 @@
|
|||||||
import {
|
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
||||||
createFileRoute,
|
|
||||||
useNavigate,
|
|
||||||
useParams,
|
|
||||||
useRouterState,
|
|
||||||
} from '@tanstack/react-router'
|
|
||||||
import { Pencil, Sparkles } from 'lucide-react'
|
import { Pencil, Sparkles } from 'lucide-react'
|
||||||
import { useCallback, useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
import type { AsignaturaDetail } from '@/data'
|
import type { AsignaturaDetail } from '@/data'
|
||||||
import type {
|
|
||||||
CampoEstructura,
|
|
||||||
IAMessage,
|
|
||||||
IASugerencia,
|
|
||||||
} from '@/types/asignatura'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
@@ -97,47 +87,6 @@ const columnParsers: Partial<Record<string, (value: unknown) => string>> = {
|
|||||||
contenido_tematico: parseContenidoTematicoToPlainText,
|
contenido_tematico: parseContenidoTematicoToPlainText,
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditableHeaderField({
|
|
||||||
value,
|
|
||||||
onSave,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
value: string | number
|
|
||||||
onSave: (val: string) => void
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const textValue = String(value)
|
|
||||||
|
|
||||||
// Manejador para cuando el usuario termina de editar (pierde el foco)
|
|
||||||
const handleBlur = (e: React.FocusEvent<HTMLSpanElement>) => {
|
|
||||||
const newValue = e.currentTarget.innerText
|
|
||||||
if (newValue !== textValue) {
|
|
||||||
onSave(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault()
|
|
||||||
e.currentTarget.blur() // Forzamos el guardado al presionar Enter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
||||||
<span
|
|
||||||
contentEditable
|
|
||||||
suppressContentEditableWarning={true} // Evita el warning de React por tener hijos y contentEditable
|
|
||||||
spellCheck={false}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
className={`inline-block cursor-text rounded-sm px-1 transition-all hover:bg-white/10 focus:bg-white/20 focus:ring-2 focus:ring-blue-400/50 focus:outline-none ${className ?? ''} `}
|
|
||||||
>
|
|
||||||
{textValue}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/planes/$planId/asignaturas/$asignaturaId',
|
'/planes/$planId/asignaturas/$asignaturaId',
|
||||||
)({
|
)({
|
||||||
@@ -145,67 +94,14 @@ export const Route = createFileRoute(
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function AsignaturaDetailPage() {
|
export default function AsignaturaDetailPage() {
|
||||||
const routerState = useRouterState()
|
|
||||||
const state = routerState.location.state as any
|
|
||||||
const { asignaturaId } = useParams({
|
const { asignaturaId } = useParams({
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
from: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
})
|
})
|
||||||
const { planId } = useParams({
|
const { data: asignaturaApi } = useSubject(asignaturaId)
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
})
|
|
||||||
const { data: asignaturaApi, isLoading: loadingAsig } =
|
|
||||||
useSubject(asignaturaId)
|
|
||||||
// 1. Asegúrate de tener estos estados en tu componente principal
|
|
||||||
const [messages, setMessages] = useState<Array<IAMessage>>([])
|
|
||||||
const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null)
|
const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null)
|
||||||
const [campos] = useState<Array<CampoEstructura>>([])
|
|
||||||
const [activeTab, setActiveTab] = useState('datos')
|
|
||||||
const updateAsignatura = useUpdateAsignatura()
|
const updateAsignatura = useUpdateAsignatura()
|
||||||
|
|
||||||
// Dentro de AsignaturaDetailPage
|
|
||||||
const [headerData, setHeaderData] = useState({
|
|
||||||
codigo: '',
|
|
||||||
nombre: '',
|
|
||||||
creditos: 0,
|
|
||||||
ciclo: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Si en el state de la ruta viene una pestaña específica, cámbiate a ella
|
|
||||||
if (state?.activeTab) {
|
|
||||||
setActiveTab(state.activeTab)
|
|
||||||
}
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
// Sincronizar cuando llegue la API
|
|
||||||
useEffect(() => {
|
|
||||||
if (asignaturaApi) {
|
|
||||||
setHeaderData({
|
|
||||||
codigo: asignaturaApi.codigo ?? '',
|
|
||||||
nombre: asignaturaApi.nombre,
|
|
||||||
creditos: asignaturaApi.creditos,
|
|
||||||
ciclo: asignaturaApi.numero_ciclo ?? 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [asignaturaApi])
|
|
||||||
|
|
||||||
const handleUpdateHeader = (key: string, value: string | number) => {
|
|
||||||
const newData = { ...headerData, [key]: value }
|
|
||||||
setHeaderData(newData)
|
|
||||||
|
|
||||||
const patch: Record<string, any> =
|
|
||||||
key === 'ciclo'
|
|
||||||
? { numero_ciclo: value }
|
|
||||||
: {
|
|
||||||
[key]: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAsignatura.mutate({
|
|
||||||
asignaturaId,
|
|
||||||
patch,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePersistDatoGeneral = (clave: string, value: string) => {
|
const handlePersistDatoGeneral = (clave: string, value: string) => {
|
||||||
const baseDatos = asignatura?.datos ?? (asignaturaApi as any)?.datos ?? {}
|
const baseDatos = asignatura?.datos ?? (asignaturaApi as any)?.datos ?? {}
|
||||||
const mergedDatos = { ...baseDatos, [clave]: value }
|
const mergedDatos = { ...baseDatos, [clave]: value }
|
||||||
@@ -228,76 +124,9 @@ export default function AsignaturaDetailPage() {
|
|||||||
if (asignaturaApi) setAsignatura(asignaturaApi)
|
if (asignaturaApi) setAsignatura(asignaturaApi)
|
||||||
}, [asignaturaApi])
|
}, [asignaturaApi])
|
||||||
|
|
||||||
// 2. Funciones de manejo para la IA
|
|
||||||
const handleSendMessage = (text: string, campoId?: string) => {
|
|
||||||
const newMessage: IAMessage = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
role: 'user',
|
|
||||||
content: text,
|
|
||||||
timestamp: new Date(),
|
|
||||||
campoAfectado: campoId,
|
|
||||||
}
|
|
||||||
setMessages([...messages, newMessage])
|
|
||||||
|
|
||||||
// Aquí llamarías a tu API de OpenAI/Claude
|
|
||||||
// toast.info("Enviando consulta a la IA...");
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAcceptSuggestion = (_sugerencia: IASugerencia) => {
|
|
||||||
// Lógica para actualizar el valor del campo en tu estado de asignatura
|
|
||||||
// toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dentro de tu componente principal (donde están los Tabs)
|
|
||||||
const [bibliografia, setBibliografia] = useState<Array<BibliografiaEntry>>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
tipo: 'BASICA',
|
|
||||||
cita: 'Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach. Pearson.',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
|
||||||
|
|
||||||
const handleSaveBibliografia = (data: Array<BibliografiaEntry>) => {
|
|
||||||
setIsSaving(true)
|
|
||||||
// Aquí iría tu llamada a la API
|
|
||||||
setBibliografia(data)
|
|
||||||
|
|
||||||
// Simulamos un guardado
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsSaving(false)
|
|
||||||
// toast.success("Cambios guardados");
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [isRegenerating, setIsRegenerating] = useState(false)
|
|
||||||
|
|
||||||
const handleRegenerateDocument = useCallback(() => {
|
|
||||||
setIsRegenerating(true)
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsRegenerating(false)
|
|
||||||
}, 2000)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return <DatosGenerales onPersistDato={handlePersistDatoGeneral} />
|
return <DatosGenerales onPersistDato={handlePersistDatoGeneral} />
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EstructuraDefinicion {
|
|
||||||
properties?: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
title?: string
|
|
||||||
description?: string
|
|
||||||
examples?: Array<string>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
}
|
|
||||||
interface DatosGeneralesProps {
|
|
||||||
asignaturaId: string
|
|
||||||
data: AsignaturaDetail | null
|
|
||||||
isLoading: boolean
|
|
||||||
onPersistDato: (clave: string, value: string) => void
|
|
||||||
}
|
|
||||||
function DatosGenerales({
|
function DatosGenerales({
|
||||||
onPersistDato,
|
onPersistDato,
|
||||||
}: {
|
}: {
|
||||||
@@ -309,10 +138,22 @@ function DatosGenerales({
|
|||||||
|
|
||||||
const { data: data, isLoading: isLoading } = useSubject(asignaturaId)
|
const { data: data, isLoading: isLoading } = useSubject(asignaturaId)
|
||||||
|
|
||||||
const structureProps =
|
// 1. Extraemos la definición de la estructura (los metadatos)
|
||||||
(data?.estructuras_asignatura?.definicion as EstructuraDefinicion)
|
const definicionRaw = data?.estructuras_asignatura?.definicion
|
||||||
.properties || {}
|
const definicion = isRecord(definicionRaw)
|
||||||
const valoresActuales = data?.datos || {}
|
? (definicionRaw as Record<string, unknown>)
|
||||||
|
: null
|
||||||
|
|
||||||
|
const propertiesRaw = definicion ? (definicion as any).properties : undefined
|
||||||
|
const structureProps = isRecord(propertiesRaw)
|
||||||
|
? (propertiesRaw as Record<string, any>)
|
||||||
|
: {}
|
||||||
|
|
||||||
|
// 2. Extraemos los valores reales (el contenido redactado)
|
||||||
|
const datosRaw = data?.datos
|
||||||
|
const valoresActuales = isRecord(datosRaw)
|
||||||
|
? (datosRaw as Record<string, any>)
|
||||||
|
: {}
|
||||||
if (isLoading) return <p>Cargando información...</p>
|
if (isLoading) return <p>Cargando información...</p>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -338,13 +179,28 @@ function DatosGenerales({
|
|||||||
const cardTitle = config.title || key
|
const cardTitle = config.title || key
|
||||||
const description = config.description || ''
|
const description = config.description || ''
|
||||||
|
|
||||||
|
const xColumn =
|
||||||
|
typeof config?.['x-column'] === 'string'
|
||||||
|
? config['x-column']
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// Obtenemos el placeholder del arreglo 'examples' de la estructura
|
||||||
const placeholder =
|
const placeholder =
|
||||||
config.examples && config.examples.length > 0
|
config.examples && config.examples.length > 0
|
||||||
? config.examples[0]
|
? config.examples[0]
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const valActual = (valoresActuales as Record<string, any>)[key]
|
const valActual = valoresActuales[key]
|
||||||
const currentContent = valActual ?? ''
|
|
||||||
|
let currentContent = valActual ?? ''
|
||||||
|
|
||||||
|
if (xColumn) {
|
||||||
|
const rawValue = (data as any)?.[xColumn]
|
||||||
|
const parser = columnParsers[xColumn]
|
||||||
|
currentContent = parser
|
||||||
|
? parser(rawValue)
|
||||||
|
: String(rawValue ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfoCard
|
<InfoCard
|
||||||
@@ -353,6 +209,7 @@ function DatosGenerales({
|
|||||||
clave={key}
|
clave={key}
|
||||||
title={cardTitle}
|
title={cardTitle}
|
||||||
initialContent={currentContent}
|
initialContent={currentContent}
|
||||||
|
xColumn={xColumn}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
description={description}
|
description={description}
|
||||||
onPersist={(clave, value) => onPersistDato(clave, value)}
|
onPersist={(clave, value) => onPersistDato(clave, value)}
|
||||||
@@ -524,12 +381,8 @@ function InfoCard({
|
|||||||
// Agregamos un timestamp para forzar la actualización
|
// Agregamos un timestamp para forzar la actualización
|
||||||
// de la location.state aunque la ruta sea la misma.
|
// de la location.state aunque la ruta sea la misma.
|
||||||
navigate({
|
navigate({
|
||||||
to: '/planes/$planId/asignaturas/$asignaturaId',
|
to: '/planes/$planId/asignaturas/$asignaturaId/contenido',
|
||||||
params: { planId, asignaturaId: asignaturaId! },
|
params: { planId, asignaturaId: asignaturaId! },
|
||||||
state: {
|
|
||||||
activeTab: 'contenido',
|
|
||||||
_ts: Date.now(),
|
|
||||||
} as any,
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user