Usar los titles de la definición de la estructura

fix #48
fix #49
This commit is contained in:
2026-01-28 12:25:51 -06:00
parent b35dcf3b54
commit 5224e632f8
2 changed files with 175 additions and 90 deletions

View File

@@ -87,7 +87,8 @@ export function IAMateriaTab({
const availableFields = useMemo(() => { const availableFields = useMemo(() => {
// Extraemos las claves directamente del objeto datosGenerales // Extraemos las claves directamente del objeto datosGenerales
// ["nombre", "descripcion", "perfil_de_egreso", "fines_de_aprendizaje_o_formacion"] // ["nombre", "descripcion", "perfil_de_egreso", "fines_de_aprendizaje_o_formacion"]
return Object.keys(datosGenerales).map((key) => { if (!datosGenerales.datos) return []
return Object.keys(datosGenerales.datos).map((key) => {
// Buscamos si existe un nombre amigable en la estructura de campos // Buscamos si existe un nombre amigable en la estructura de campos
const estructuraCampo = campos.find((c) => c.id === key) const estructuraCampo = campos.find((c) => c.id === key)
@@ -110,12 +111,15 @@ export function IAMateriaTab({
const state = routerState.location.state as any const state = routerState.location.state as any
if (state?.prefillCampo && availableFields.length > 0) { if (state?.prefillCampo && availableFields.length > 0) {
console.log(state?.prefillCampo)
console.log(availableFields)
const field = availableFields.find((f) => f.key === state.prefillCampo) const field = availableFields.find((f) => f.key === state.prefillCampo)
if (field && !selectedFields.find((sf) => sf.key === field.key)) { if (field && !selectedFields.find((sf) => sf.key === field.key)) {
setSelectedFields([field]) setSelectedFields([field])
// Sincronizamos el texto inicial con el campo pre-seleccionado // Sincronizamos el texto inicial con el campo pre-seleccionado
setInput(`Mejora el campo ${field.key}: `) setInput(`Mejora el campo ${field.label}: `)
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -139,37 +143,32 @@ export function IAMateriaTab({
setSelectedFields((prev) => { setSelectedFields((prev) => {
const isSelected = prev.find((f) => f.key === field.key) const isSelected = prev.find((f) => f.key === field.key)
// Si lo estamos seleccionando (no estaba antes) // 1. Si ya está seleccionado, lo quitamos (Toggle OFF)
if (!isSelected) { if (isSelected) {
// Actualizamos el texto del input: return prev.filter((f) => f.key !== field.key)
// Si termina en ":", lo reemplazamos por el key para que sea "Mejora perfil_de_egreso "
// Si no, simplemente lo añadimos al final.
setInput((prevText) => {
const [beforeColon, afterColon = ''] = prevText.split(':')
// Campos ya escritos después de :
const existingKeys = afterColon
.split(',')
.map((k) => k.trim())
.filter(Boolean)
// Si ya existe, no lo volvemos a agregar
if (existingKeys.includes(field.key)) {
return prevText
}
const updatedKeys = [...existingKeys, field.key].join(', ')
return `${beforeColon.trim()}: ${updatedKeys} `
})
return [field]
} }
// Si lo estamos deseleccionando, solo quitamos el tag // 2. Si no está, lo agregamos a la lista (Toggle ON)
return prev.filter((f) => f.key !== field.key) const newSelected = [...prev, field]
// 3. Actualizamos el texto del input para reflejar los títulos (labels)
setInput((prevText) => {
// Separamos lo que el usuario escribió antes del disparador ":"
// y lo que viene después (posibles keys/labels previos)
const parts = prevText.split(':')
const beforeColon = parts[0]
// Creamos un string con los labels de todos los campos seleccionados
const labelsPath = newSelected.map((f) => f.label).join(', ')
return `${beforeColon.trim()}: ${labelsPath} `
})
return newSelected
}) })
setShowSuggestions(false)
// Opcional: mantener abierto si quieres que el usuario elija varios seguidos
// setShowSuggestions(false)
} }
const buildPrompt = (userInput: string) => { const buildPrompt = (userInput: string) => {

View File

@@ -22,6 +22,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useSubject } from '@/data/hooks/useSubjects' import { useSubject } from '@/data/hooks/useSubjects'
import { import {
mockMateria, mockMateria,
@@ -150,7 +156,7 @@ export default function MateriaDetailPage() {
/* ---------- sincronizar API ---------- */ /* ---------- sincronizar API ---------- */
useEffect(() => { useEffect(() => {
if (asignaturasApi?.datos) { if (asignaturasApi?.datos) {
setDatosGenerales(asignaturasApi.datos) setDatosGenerales(asignaturasApi)
} }
}, [asignaturasApi]) }, [asignaturasApi])
@@ -392,6 +398,13 @@ function DatosGenerales({
const formatTitle = (key: string): string => const formatTitle = (key: string): string =>
key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()) 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 ( return (
<div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500"> <div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
{/* Encabezado de la Sección */} {/* Encabezado de la Sección */}
@@ -413,19 +426,47 @@ function DatosGenerales({
{isLoading && <p>Cargando información...</p>} {isLoading && <p>Cargando información...</p>}
{!isLoading && {!isLoading &&
Object.entries(data).map(([key, value]) => ( Object.entries(structureProps).map(
<InfoCard ([key, config]: [string, any]) => {
asignaturaId={asignaturaId} // 1. METADATOS (Vienen de structureProps -> config)
key={key} const cardTitle = config.title || key
clave={key} const description = config.description || ''
title={formatTitle(key)}
initialContent={value} // Obtenemos el placeholder del arreglo 'examples' de la estructura
onEnhanceAI={(contenido) => { const placeholder =
console.log('Llevar a IA:', contenido) config.examples && config.examples.length > 0
// Aquí tu lógica: setPestañaActiva('mejorar-con-ia'); ? 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 (
<InfoCard
asignaturaId={asignaturaId}
key={key}
clave={key}
title={cardTitle}
initialContent={currentContent} // Si es igual a la descripción de la SEP, pasamos vacío
placeholder={placeholder} // Aquí irá "Primer semestre", "MAT-101", etc.
description={description} // El texto largo de "Indicar el ciclo..."
onEnhanceAI={(contenido) => console.log(contenido)}
/>
)
},
)}
</div> </div>
{/* Columna Lateral (Información Secundaria) */} {/* Columna Lateral (Información Secundaria) */}
@@ -469,11 +510,14 @@ function DatosGenerales({
interface InfoCardProps { interface InfoCardProps {
asignaturaId?: string asignaturaId?: string
clave: string clave?: string
title: string title: string
initialContent: any initialContent: any
placeholder?: string
description?: string
required?: boolean // Nueva prop para el asterisco
type?: 'text' | 'requirements' | 'evaluation' type?: 'text' | 'requirements' | 'evaluation'
onEnhanceAI?: (content: any) => void // Nueva prop para la acción de IA onEnhanceAI?: (content: any) => void
} }
function InfoCard({ function InfoCard({
@@ -481,76 +525,111 @@ function InfoCard({
clave, clave,
title, title,
initialContent, initialContent,
placeholder,
description,
required,
type = 'text', type = 'text',
onEnhanceAI,
}: InfoCardProps) { }: InfoCardProps) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [data, setData] = useState(initialContent) const [data, setData] = useState(initialContent)
const [tempText, setTempText] = useState( const [tempText, setTempText] = useState(initialContent)
type === 'text' ? initialContent : JSON.stringify(initialContent, null, 2),
)
const navigate = useNavigate() const navigate = useNavigate()
useEffect(() => {
setData(initialContent)
setTempText(initialContent)
}, [initialContent])
const handleSave = () => { const handleSave = () => {
setData(tempText) setData(tempText)
setIsEditing(false) setIsEditing(false)
// Aquí iría tu lógica de guardado a la DB
} }
const handleIARequest = (data) => {
console.log(data) const handleIARequest = (campoClave: string) => {
console.log(asignaturaId) console.log(placeholder)
navigate({ navigate({
to: '/planes/$planId/asignaturas/$asignaturaId', to: '/planes/$planId/asignaturas/$asignaturaId',
params: { params: { asignaturaId: asignaturaId! },
asignaturaId: asignaturaId,
},
state: { state: {
activeTab: 'ia', activeTab: 'ia',
prefillCampo: data, prefillCampo: campoClave,
prefillContenido: data, // el contenido actual del card prefillContenido: data,
} as any, } as any,
}) })
} }
return ( return (
<Card className="transition-all hover:border-slate-300"> <Card className="overflow-hidden transition-all hover:border-slate-300">
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3"> <TooltipProvider>
<CardTitle className="text-sm font-bold text-slate-700"> <CardHeader className="border-b bg-slate-50/50 px-5 py-3">
{title} <div className="flex items-center justify-between">
</CardTitle> <div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<CardTitle className="cursor-help text-sm font-bold text-slate-700">
{title}
</CardTitle>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-xs">
{description || 'Información del campo'}
</TooltipContent>
</Tooltip>
{!isEditing && ( {required && (
<div className="flex gap-1"> <span
{/* NUEVO: Botón de Mejorar con IA */} className="text-sm font-bold text-red-500"
<Button title="Requerido"
variant="ghost" >
size="icon" *
className="h-8 w-8 text-blue-500 hover:bg-blue-50 hover:text-blue-600" </span>
onClick={() => handleIARequest(clave)} // Enviamos la data actual a la IA )}
title="Mejorar con IA" </div>
>
<Sparkles className="h-4 w-4" />
</Button>
{/* Botón de Editar original */} {!isEditing && (
<Button <div className="flex gap-1">
variant="ghost" <Tooltip>
size="icon" <TooltipTrigger asChild>
className="h-8 w-8 text-slate-400" <Button
onClick={() => setIsEditing(true)} variant="ghost"
> size="icon"
<Pencil className="h-3 w-3" /> className="h-8 w-8 text-blue-500 hover:bg-blue-100"
</Button> onClick={() => handleIARequest(clave)}
>
<Sparkles className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Mejorar con IA</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400"
onClick={() => setIsEditing(true)}
>
<Pencil className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Editar campo</TooltipContent>
</Tooltip>
</div>
)}
</div> </div>
)} </CardHeader>
</CardHeader> </TooltipProvider>
<CardContent> <CardContent className="pt-4">
{isEditing ? ( {isEditing ? (
<div className="space-y-3"> <div className="space-y-3">
<Textarea <Textarea
value={tempText} value={tempText}
placeholder={placeholder}
onChange={(e) => setTempText(e.target.value)} onChange={(e) => setTempText(e.target.value)}
className="min-h-[100px] text-xs" className="min-h-[120px] text-sm leading-relaxed"
/> />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button <Button
@@ -570,10 +649,17 @@ function InfoCard({
</div> </div>
</div> </div>
) : ( ) : (
<div className="text-sm"> <div className="text-sm leading-relaxed text-slate-600">
{type === 'text' &&
(data ? (
<p className="whitespace-pre-wrap">{data}</p>
) : (
<p className="text-slate-400 italic">
Sin información. Ejemplo: {placeholder}
</p>
))}
{type === 'requirements' && <RequirementsView items={data} />} {type === 'requirements' && <RequirementsView items={data} />}
{type === 'evaluation' && <EvaluationView items={data} />} {type === 'evaluation' && <EvaluationView items={data} />}
{type === 'text' && <p className="text-slate-600">{data}</p>}
</div> </div>
)} )}
</CardContent> </CardContent>