diff --git a/src/routes/planes/$planId/_detalle/datos.tsx b/src/routes/planes/$planId/_detalle/datos.tsx index be61848..2e6efca 100644 --- a/src/routes/planes/$planId/_detalle/datos.tsx +++ b/src/routes/planes/$planId/_detalle/datos.tsx @@ -1,12 +1,13 @@ -import { usePlan } from '@/data' -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { Pencil, Check, X, Sparkles, AlertCircle } from 'lucide-react' import { useState, useEffect } from 'react' + import type { DatosGeneralesField } from '@/types/plan' + import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' -import { Pencil, Check, X, Sparkles, AlertCircle } from 'lucide-react' -//import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea - +import { usePlan } from '@/data' +// import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea export const Route = createFileRoute('/planes/$planId/_detalle/datos')({ component: DatosGeneralesPage, }) @@ -18,9 +19,9 @@ const formatLabel = (key: string) => { function DatosGeneralesPage() { const { data } = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f') - + const navigate = useNavigate() // Inicializamos campos como un arreglo vacío - const [campos, setCampos] = useState([]) + const [campos, setCampos] = useState>([]) const [editingId, setEditingId] = useState(null) const [editValue, setEditValue] = useState('') @@ -30,7 +31,7 @@ function DatosGeneralesPage() { const sourceData = data?.datos if (sourceData && typeof sourceData === 'object') { - const datosTransformados: DatosGeneralesField[] = Object.entries( + const datosTransformados: Array = Object.entries( sourceData, ).map(([key, value], index) => ({ id: (index + 1).toString(), @@ -43,6 +44,7 @@ function DatosGeneralesPage() { setCampos(datosTransformados) } + console.log(data) }, [data]) // 3. Manejadores de acciones (Ahora como funciones locales) @@ -63,14 +65,20 @@ function DatosGeneralesPage() { ) setEditingId(null) setEditValue('') - //toast.success('Cambios guardados localmente') + // toast.success('Cambios guardados localmente') } - const handleIARequest = (id: string) => { - //toast.info('La IA está analizando el campo ' + id) - // Aquí conectarías con tu endpoint de IA + const handleIARequest = (descripcion: string) => { + navigate({ + to: '/planes/$planId/iaplan', + params: { + planId: '1', // o dinámico + }, + state: { + prefill: descripcion, + } as any, + }) } - return (
@@ -112,7 +120,7 @@ function DatosGeneralesPage() { variant="ghost" size="icon" className="h-8 w-8 text-teal-600" - onClick={() => handleIARequest(campo.id)} + onClick={() => handleIARequest(campo.value)} > diff --git a/src/routes/planes/$planId/_detalle/iaplan.tsx b/src/routes/planes/$planId/_detalle/iaplan.tsx index f25d24e..dea5a3d 100644 --- a/src/routes/planes/$planId/_detalle/iaplan.tsx +++ b/src/routes/planes/$planId/_detalle/iaplan.tsx @@ -1,23 +1,23 @@ -import { createFileRoute } from '@tanstack/react-router' -import { useState, useEffect, useRef } from 'react' +import { createFileRoute, useRouterState } from '@tanstack/react-router' import { Sparkles, Send, - Paperclip, Target, UserCheck, Lightbulb, FileText, - Users, GraduationCap, BookOpen, Check, X, } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Textarea } from '@/components/ui/textarea' -import { ScrollArea } from '@/components/ui/scroll-area' +import { useState, useEffect, useRef, useMemo } from 'react' + import { Avatar, AvatarFallback } from '@/components/ui/avatar' +import { Button } from '@/components/ui/button' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Textarea } from '@/components/ui/textarea' +import { usePlan } from '@/data/hooks/usePlans' const PRESETS = [ { @@ -46,100 +46,154 @@ const PRESETS = [ }, ] +// --- Tipado y Helpers --- +interface SelectedField { + key: string + label: 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')({ component: RouteComponent, }) -interface Message { - id: string - role: 'user' | 'assistant' - content: string -} - function RouteComponent() { - const [messages, setMessages] = useState([ + 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 routerState = useRouterState() + + // ESTADOS PRINCIPALES + const [messages, setMessages] = useState>([ { id: '1', role: 'assistant', - content: '¡Hola! Soy tu asistente de IA. ¿En qué puedo ayudarte hoy?', + content: + '¡Hola! Soy tu asistente de IA. ¿Qué campos deseas mejorar? Puedes escribir ":" para seleccionar uno.', }, ]) const [input, setInput] = useState('') + const [selectedFields, setSelectedFields] = useState>([]) + const [showSuggestions, setShowSuggestions] = useState(false) const [isLoading, setIsLoading] = useState(false) - const [pendingSuggestion, setPendingSuggestion] = useState<{ - field: string - text: string - } | null>(null) + const [pendingSuggestion, setPendingSuggestion] = useState(null) const scrollRef = useRef(null) - // Función de scroll corregida para Radix - const scrollToBottom = () => { - const viewport = scrollRef.current?.querySelector( - '[data-radix-scroll-area-viewport]', - ) - if (viewport) { - viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' }) + // 1. Transformar datos de la API para el menú de selección + const availableFields = useMemo(() => { + if (!data?.datos) return [] + return Object.entries(data.datos).map(([key, value]) => ({ + key, + label: formatLabel(key), + value: String(value || ''), + })) + }, [data]) + + // 2. Manejar el estado inicial si viene de "Datos Generales" + useEffect(() => { + const state = routerState.location.state as any + if (state?.prefill && availableFields.length > 0) { + // Intentamos encontrar qué campo es por su valor o si mandaste el fieldKey + const field = availableFields.find( + (f) => f.value === state.prefill || f.key === state.fieldKey, + ) + if (field && !selectedFields.find((sf) => sf.key === field.key)) { + setSelectedFields([field]) + } + setInput(`Mejora este campo: `) + } + }, [availableFields]) + + // 3. Lógica para el disparador ":" + const handleInputChange = (e: React.ChangeEvent) => { + const val = e.target.value + setInput(val) + if (val.endsWith(':')) { + setShowSuggestions(true) + } else { + setShowSuggestions(false) } } - useEffect(() => { - const timer = setTimeout(scrollToBottom, 100) - return () => clearTimeout(timer) - }, [messages, isLoading]) + const toggleField = (field: SelectedField) => { + setSelectedFields((prev) => + prev.find((f) => f.key === field.key) + ? prev.filter((f) => f.key !== field.key) + : [...prev, field], + ) + if (input.endsWith(':')) setInput(input.slice(0, -1)) + setShowSuggestions(false) + } - const handleSend = async (prompt?: string) => { - const messageText = prompt || input - if (!messageText.trim()) return + const handleSend = async (promptOverride?: string) => { + const textToSend = promptOverride || input + if (!textToSend.trim() && selectedFields.length === 0) return - const userMessage: Message = { + const userMsg = { id: Date.now().toString(), role: 'user', - content: messageText, + content: textToSend, } - setMessages((prev) => [...prev, userMessage]) + setMessages((prev) => [...prev, userMsg]) setInput('') setIsLoading(true) + // Aquí simularías la llamada a la API enviando 'selectedFields' como contexto setTimeout(() => { const mockText = - 'He analizado tu solicitud. Basado en los estándares actuales, sugiero fortalecer las competencias técnicas...' - const aiResponse: Message = { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: `He analizado tu solicitud. Aquí está mi sugerencia:\n\n"${mockText}"\n\n¿Te gustaría aplicar este texto al plan?`, - } - setMessages((prev) => [...prev, aiResponse]) - setPendingSuggestion({ field: 'seccion-plan', text: mockText }) + 'Sugerencia generada basada en los campos seleccionados...' + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + role: 'assistant', + content: `He analizado ${selectedFields.length > 0 ? selectedFields.map((f) => f.label).join(', ') : 'tu solicitud'}. Aquí tienes una propuesta:\n\n${mockText}`, + }, + ]) + setPendingSuggestion({ text: mockText }) setIsLoading(false) }, 1200) } return ( - /* CAMBIO CLAVE 1: - Aseguramos que el contenedor padre ocupe el espacio disponible pero NO MÁS. - 'max-h-full' y 'flex-1' evitan que el chat empuje el layout hacia abajo. - */
- {/* PANEL DE CHAT */} + {/* PANEL DE CHAT PRINCIPAL */}
- {/* Header Fijo (shrink-0 es vital para que no se aplaste) */} -
-
-

- - Asistente de Diseño Curricular -

-

- Optimizado con IA -

+ {/* NUEVO: Barra superior de campos seleccionados */} +
+
+ + Campos a mejorar: + + {selectedFields.map((field) => ( +
+ {field.label} + +
+ ))} + {selectedFields.length === 0 && ( + + Escribe ":" para añadir campos + + )}
- {/* CAMBIO CLAVE 2: - El ScrollArea debe tener 'flex-1' y 'h-full'. - Esto obliga al componente a colapsar su altura y activar el scroll. - */} + {/* CONTENIDO DEL CHAT */}
@@ -162,16 +216,11 @@ function RouteComponent() {
- {msg.role === 'assistant' && ( - - Asistente IA - - )}
{msg.content} @@ -189,9 +238,9 @@ function RouteComponent() {
- {/* Barra de aplicación flotante (dentro del contenedor relativo del scroll) */} + {/* Botones flotantes de aplicación */} {pendingSuggestion && !isLoading && ( -
+
- {/* INPUT FIJO AL FONDO */} + {/* INPUT FIJO AL FONDO CON SUGERENCIAS : */} + {/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
-
-