diff --git a/src/components/asignaturas/detalle/IAMateriaTab.tsx b/src/components/asignaturas/detalle/IAMateriaTab.tsx index f3d7c0b..faffe89 100644 --- a/src/components/asignaturas/detalle/IAMateriaTab.tsx +++ b/src/components/asignaturas/detalle/IAMateriaTab.tsx @@ -1,357 +1,406 @@ -import { useState, useRef, useEffect } from 'react'; -import { Send, Sparkles, Bot, User, Check, X, RefreshCw, Lightbulb, Wand2 } from 'lucide-react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Textarea } from '@/components/ui/textarea'; -import { Badge } from '@/components/ui/badge'; -import { ScrollArea } from '@/components/ui/scroll-area'; +import { useRouterState } from '@tanstack/react-router' import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from '@/components/ui/command'; -import type { IAMessage, IASugerencia, CampoEstructura } from '@/types/materia'; -import { cn } from '@/lib/utils'; -//import { toast } from 'sonner'; + Sparkles, + Send, + Target, + UserCheck, + Lightbulb, + FileText, + GraduationCap, + BookOpen, + Check, + X, +} from 'lucide-react' +import { useState, useEffect, useRef, useMemo } from 'react' -interface IAMateriaTabProps { - campos: CampoEstructura[]; - datosGenerales: Record; - messages: IAMessage[]; - onSendMessage: (message: string, campoId?: string) => void; - onAcceptSuggestion: (sugerencia: IASugerencia) => void; - onRejectSuggestion: (messageId: string) => void; +import type { IAMessage, IASugerencia, CampoEstructura } from '@/types/materia' + +import { Avatar, AvatarFallback } from '@/components/ui/avatar' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Textarea } from '@/components/ui/textarea' +import { cn } from '@/lib/utils' + +// Tipos importados de tu archivo de materia + +const PRESETS = [ + { + id: 'mejorar-objetivo', + label: 'Mejorar objetivo', + icon: Target, + prompt: 'Mejora la redacción del objetivo de esta asignatura...', + }, + { + id: 'contenido-tematico', + label: 'Sugerir contenido', + icon: BookOpen, + prompt: 'Genera un desglose de temas para esta materia...', + }, + { + id: 'actividades', + label: 'Actividades de aprendizaje', + icon: GraduationCap, + prompt: 'Sugiere actividades prácticas para los temas seleccionados...', + }, + { + id: 'bibliografia', + label: 'Actualizar bibliografía', + icon: FileText, + prompt: 'Recomienda bibliografía reciente para esta asignatura...', + }, +] + +interface SelectedField { + key: string + label: string + value: string } -const quickActions = [ - { id: 'mejorar-objetivos', label: 'Mejorar objetivos', icon: Wand2, prompt: 'Mejora el :objetivo_general para que sea más específico y medible' }, - { id: 'generar-contenido', label: 'Generar contenido temático', icon: Lightbulb, prompt: 'Sugiere un contenido temático completo basado en los objetivos y competencias' }, - { id: 'alinear-perfil', label: 'Alinear con perfil de egreso', icon: RefreshCw, prompt: 'Revisa las :competencias y alinéalas con el perfil de egreso del plan' }, - { id: 'ajustar-biblio', label: 'Recomendar bibliografía', icon: Sparkles, prompt: 'Recomienda bibliografía actualizada basándote en el contenido temático' }, -]; +interface IAMateriaTabProps { + campos: Array + datosGenerales: Record + messages: Array + onSendMessage: (message: string, campoId?: string) => void + onAcceptSuggestion: (sugerencia: IASugerencia) => void + onRejectSuggestion: (messageId: string) => void +} -export function IAMateriaTab({ campos, datosGenerales, messages, onSendMessage, onAcceptSuggestion, onRejectSuggestion }: IAMateriaTabProps) { - const [input, setInput] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [showFieldSelector, setShowFieldSelector] = useState(false); - const [fieldSelectorPosition, setFieldSelectorPosition] = useState({ top: 0, left: 0 }); - const [cursorPosition, setCursorPosition] = useState(0); - const textareaRef = useRef(null); - const scrollRef = useRef(null); +export function IAMateriaTab({ + campos, + datosGenerales, + messages, + onSendMessage, + onAcceptSuggestion, + onRejectSuggestion, +}: IAMateriaTabProps) { + const routerState = useRouterState() + + // ESTADOS PRINCIPALES (Igual que en Planes) + const [input, setInput] = useState('') + const [selectedFields, setSelectedFields] = useState>([]) + const [showSuggestions, setShowSuggestions] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const scrollRef = useRef(null) + + // 1. Transformar datos de la materia para el menú + const availableFields = useMemo(() => { + // Extraemos las claves directamente del objeto datosGenerales + // ["nombre", "descripcion", "perfil_de_egreso", "fines_de_aprendizaje_o_formacion"] + return Object.keys(datosGenerales).map((key) => { + // Buscamos si existe un nombre amigable en la estructura de campos + const estructuraCampo = campos.find((c) => c.id === key) + + // Si existe en 'campos', usamos su nombre; si no, formateamos la clave (ej: perfil_de_egreso -> Perfil De Egreso) + const labelAmigable = + estructuraCampo?.nombre || + key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()) + + return { + key: key, + label: labelAmigable, + value: String(datosGenerales[key] || ''), + } + }) + }, [campos, datosGenerales]) + + // 2. Manejar el estado inicial si viene de "Datos de Materia" (Prefill) + useEffect(() => { + const state = routerState.location.state as any + + if (state?.prefillCampo && availableFields.length > 0) { + const field = availableFields.find((f) => f.key === state.prefillCampo) + + if (field && !selectedFields.find((sf) => sf.key === field.key)) { + setSelectedFields([field]) + // Sincronizamos el texto inicial con el campo pre-seleccionado + setInput(`Mejora el campo ${field.key}: `) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [availableFields]) + + // Scroll automático useEffect(() => { if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight } - }, [messages]); + }, [messages, isLoading]) + // 3. Lógica para el disparador ":" const handleInputChange = (e: React.ChangeEvent) => { - const value = e.target.value; - const pos = e.target.selectionStart; - setInput(value); - setCursorPosition(pos); + const val = e.target.value + setInput(val) + setShowSuggestions(val.endsWith(':')) + } - // Check for : character to trigger field selector - const lastChar = value.charAt(pos - 1); - if (lastChar === ':') { - const rect = textareaRef.current?.getBoundingClientRect(); - if (rect) { - setFieldSelectorPosition({ top: rect.bottom + 8, left: rect.left }); - setShowFieldSelector(true); + const toggleField = (field: SelectedField) => { + setSelectedFields((prev) => { + const isSelected = prev.find((f) => f.key === field.key) + + // Si lo estamos seleccionando (no estaba antes) + if (!isSelected) { + // Actualizamos el texto del input: + // 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] } - } else if (showFieldSelector && (lastChar === ' ' || !value.includes(':'))) { - setShowFieldSelector(false); - } - }; - const insertFieldMention = (campoId: string) => { - const beforeCursor = input.slice(0, cursorPosition); - const afterCursor = input.slice(cursorPosition); - const lastColonIndex = beforeCursor.lastIndexOf(':'); - const newInput = beforeCursor.slice(0, lastColonIndex) + `:${campoId}` + afterCursor; - setInput(newInput); - setShowFieldSelector(false); - textareaRef.current?.focus(); - }; + // Si lo estamos deseleccionando, solo quitamos el tag + return prev.filter((f) => f.key !== field.key) + }) + setShowSuggestions(false) + } - const handleSend = async () => { - if (!input.trim() || isLoading) return; + const buildPrompt = (userInput: string) => { + if (selectedFields.length === 0) return userInput + const fieldsText = selectedFields + .map((f) => `- ${f.label}: ${f.value || '(vacio)'}`) + .join('\n') - // Extract field mention if any - const fieldMatch = input.match(/:(\w+)/); - const campoId = fieldMatch ? fieldMatch[1] : undefined; + return `${userInput}\n\nCampos a analizar:\n${fieldsText}`.trim() + } - setIsLoading(true); - onSendMessage(input, campoId); - setInput(''); + const handleSend = async (promptOverride?: string) => { + const rawText = promptOverride || input + if (!rawText.trim() && selectedFields.length === 0) return - // Simulate AI response delay - setTimeout(() => { - setIsLoading(false); - }, 1500); - }; + const finalPrompt = buildPrompt(rawText) - const handleQuickAction = (prompt: string) => { - setInput(prompt); - textareaRef.current?.focus(); - }; + setIsLoading(true) + // Llamamos a la función que viene por props + onSendMessage(finalPrompt, selectedFields[0]?.key) - const renderMessageContent = (content: string) => { - // Render field mentions as styled badges - return content.split(/(:[\w_]+)/g).map((part, i) => { - if (part.startsWith(':')) { - const campo = campos.find(c => c.id === part.slice(1)); - return ( - - {campo?.nombre || part} - - ); - } - return part; - }); - }; + setInput('') + setSelectedFields([]) + + // Simular carga local para el feedback visual + setTimeout(() => setIsLoading(false), 1200) + } return ( -
-
-
-

- - IA de la materia -

-

- Usa : para mencionar campos específicos -

+
+ {/* PANEL DE CHAT PRINCIPAL */} +
+ {/* Barra superior */} +
+
+ + IA de Asignatura + +
-
-
- {/* Chat area */} - - - - Conversación - - - - -
- {messages.length === 0 ? ( -
- -

- Inicia una conversación para mejorar tu materia con IA -

-
- ) : ( - messages.map((message) => ( -
- {message.role === 'assistant' && ( -
- -
+ {/* CONTENIDO DEL CHAT */} +
+ +
+ {messages.map((msg) => ( +
+ + + {msg.role === 'assistant' ? ( + + ) : ( + )} -
-

- {renderMessageContent(message.content)} -

- {message.sugerencia && !message.sugerencia.aceptada && ( -
-

- Sugerencia para: {message.sugerencia.campoNombre} -

-
- {message.sugerencia.valorSugerido} -
-
- - -
+ + +
+
+ {msg.content} +
+ + {/* Renderizado de Sugerencias (Homologado con lógica de Materia) */} + {msg.sugerencia && !msg.sugerencia.aceptada && ( +
+
+

+ Propuesta para: {msg.sugerencia.campoNombre} +

+
+ {msg.sugerencia.valorSugerido} +
+
+ +
- )} - {message.sugerencia?.aceptada && ( - - - Sugerencia aplicada - - )} -
- {message.role === 'user' && ( -
-
- )} -
- )) - )} - {isLoading && ( -
-
- -
-
-
-
-
-
-
+ )} + {msg.sugerencia?.aceptada && ( + + Sugerencia aplicada + + )}
- )} -
- +
+ ))} + {isLoading && ( +
+
+
+
+
+ )} +
+ +
- {/* Input area */} -
-
+ {/* INPUT FIJO AL FONDO */} +
+
+ {/* MENÚ DE SUGERENCIAS FLOTANTE */} + {showSuggestions && ( +
+
+ Seleccionar campo de materia +
+
+ {availableFields.map((field) => ( + + ))} +
+
+ )} + + {/* CONTENEDOR DEL INPUT */} +
+ {/* Visualización de Tags */} + {selectedFields.length > 0 && ( +
+ {selectedFields.map((field) => ( +
+ Campo: {field.label} + +
+ ))} +
+ )} + +