import { createFileRoute, Link, useNavigate, useParams, useRouterState, } from '@tanstack/react-router' import { ArrowLeft, GraduationCap, Pencil, Sparkles } from 'lucide-react' import { useCallback, useState, useEffect } from 'react' import { BibliographyItem } from './BibliographyItem' import { ContenidoTematico } from './ContenidoTematico' import { DocumentoSEPTab } from './DocumentoSEPTab' import { HistorialTab } from './HistorialTab' import { IAAsignaturaTab } from './IAAsignaturaTab' import type { CampoEstructura, IAMessage, IASugerencia, } from '@/types/asignatura' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Textarea } from '@/components/ui/textarea' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { useSubject } from '@/data/hooks/useSubjects' import { mockAsignatura, mockEstructura, mockDocumentoSep, } from '@/data/mockAsignaturaData' export interface BibliografiaEntry { id: string tipo: 'BASICA' | 'COMPLEMENTARIA' cita: string fuenteBibliotecaId?: string fuenteBiblioteca?: any } export interface BibliografiaTabProps { id: string bibliografia: Array onSave: (bibliografia: Array) => void isSaving: boolean } export interface AsignaturaDatos { [key: string]: string } export interface AsignaturaResponse { datos: AsignaturaDatos } 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) => { const newValue = e.currentTarget.innerText if (newValue !== textValue) { onSave(newValue) } } const handleKeyDown = (e: React.KeyboardEvent) => { 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 {textValue} ) } export const Route = createFileRoute( '/planes/$planId/asignaturas/$asignaturaId', )({ component: AsignaturaDetailPage, }) export default function AsignaturaDetailPage() { const routerState = useRouterState() const state = routerState.location.state as any const { asignaturaId } = useParams({ from: '/planes/$planId/asignaturas/$asignaturaId', }) const { planId } = useParams({ from: '/planes/$planId/asignaturas/$asignaturaId', }) const { data: asignaturasApi, isLoading: loadingAsig } = useSubject(asignaturaId) // 1. Asegúrate de tener estos estados en tu componente principal const [messages, setMessages] = useState>([]) const [datosGenerales, setDatosGenerales] = useState({}) const [campos, setCampos] = useState>([]) const [activeTab, setActiveTab] = useState('datos') // 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 (asignaturasApi) { setHeaderData({ codigo: asignaturasApi.codigo ?? '', nombre: asignaturasApi.nombre, creditos: asignaturasApi.creditos, ciclo: asignaturasApi.numero_ciclo ?? 0, }) } }, [asignaturasApi]) const handleUpdateHeader = (key: string, value: string | number) => { const newData = { ...headerData, [key]: value } setHeaderData(newData) console.log('💾 Guardando en estado y base de datos:', key, value) } /* ---------- sincronizar API ---------- */ useEffect(() => { if (asignaturasApi?.datos) { setDatosGenerales(asignaturasApi) } }, [asignaturasApi]) // 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 datosGenerales // toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`); } // Dentro de tu componente principal (donde están los Tabs) const [bibliografia, setBibliografia] = useState>([ { 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) => { 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 (
{/* ================= HEADER ACTUALIZADO ================= */}
Volver al plan
{/* CÓDIGO EDITABLE */} handleUpdateHeader('codigo', val)} /> {/* NOMBRE EDITABLE */}

handleUpdateHeader('nombre', val)} />

{/* Eliminamos el max-w y dejamos que el flex-wrap haga su trabajo */} handleUpdateHeader('plan_nombre', val)} className="min-w-[10ch] text-blue-100" // min-w para que sea clickeable si está vacío /> handleUpdateHeader('facultad_nombre', val)} className="min-w-[10ch] text-blue-100" />

Pertenece al plan:{' '} {asignaturasApi?.planes_estudio?.nombre}

{/* CRÉDITOS EDITABLES */} handleUpdateHeader('creditos', parseInt(val) || 0) } /> créditos {/* SEMESTRE EDITABLE */} handleUpdateHeader('ciclo', parseInt(val) || 0) } /> ° ciclo {asignaturasApi?.tipo}
{/* ================= TABS ================= */}
Datos generales Contenido temático Bibliografía IA de la asignatura Documento SEP Historial {/* ================= TAB: DATOS GENERALES ================= */} console.log( 'Rechazada', ) /* toast.error("Sugerencia rechazada")*/ } />
) } /* ================= TAB CONTENT ================= */ interface DatosGeneralesProps { asignaturaId: string data: AsignaturaDatos isLoading: boolean } function DatosGenerales({ data, isLoading, asignaturaId, }: DatosGeneralesProps) { const formatTitle = (key: string): string => key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()) // 1. Extraemos la definición de la estructura (los metadatos) const structureProps = data?.estructuras_asignatura?.definicion?.properties || {} // 2. Extraemos los valores reales (el contenido redactado) const valoresActuales = data?.datos || {} return (
{/* Encabezado de la Sección */}

Datos Generales

Información oficial estructurada bajo los lineamientos de la SEP.

{/* Grid de Información */}
{/* Columna Principal (Más ancha) */}
{isLoading &&

Cargando información...

} {!isLoading && Object.entries(structureProps).map( ([key, config]: [string, any]) => { // 1. METADATOS (Vienen de structureProps -> config) const cardTitle = config.title || key const description = config.description || '' // Obtenemos el placeholder del arreglo 'examples' de la estructura const placeholder = config.examples && config.examples.length > 0 ? config.examples[0] : '' // 2. CONTENIDO REAL (Viene de data.datos -> valoresActuales) // El problema: Si 'description' en 'datos' es igual a la de la 'estructura', // el usuario aún no ha redactado nada real. const valActual = valoresActuales[key] // Lógica para determinar si mostrar el contenido o dejarlo vacío (para que salga el placeholder) // Si el contenido en 'datos' es idéntico a la instrucción de la 'estructura', // asumimos que no hay contenido real todavía. const isContentEmpty = !valActual?.description || valActual.description === config.description const currentContent = valActual?.description ?? '' return ( console.log(contenido)} /> ) }, )}
{/* Columna Lateral (Información Secundaria) */}
{/* Tarjeta de Requisitos */} {/* Tarjeta de Evaluación */}
) } interface InfoCardProps { asignaturaId?: string clave?: string title: string initialContent: any placeholder?: string description?: string required?: boolean // Nueva prop para el asterisco type?: 'text' | 'requirements' | 'evaluation' onEnhanceAI?: (content: any) => void } function InfoCard({ asignaturaId, clave, title, initialContent, placeholder, description, required, type = 'text', }: InfoCardProps) { const [isEditing, setIsEditing] = useState(false) const [data, setData] = useState(initialContent) const [tempText, setTempText] = useState(initialContent) const navigate = useNavigate() useEffect(() => { setData(initialContent) setTempText(initialContent) }, [initialContent]) const handleSave = () => { setData(tempText) setIsEditing(false) // Aquí iría tu lógica de guardado a la DB } const handleIARequest = (campoClave: string) => { console.log(placeholder) navigate({ to: '/planes/$planId/asignaturas/$asignaturaId', params: { asignaturaId: asignaturaId! }, state: { activeTab: 'ia', prefillCampo: campoClave, prefillContenido: data, } as any, }) } return (
{title} {description || 'Información del campo'} {required && ( * )}
{!isEditing && (
Mejorar con IA Editar campo
)}
{isEditing ? (