Que no haga scroll #193

This commit is contained in:
2026-03-19 14:18:21 -06:00
parent 670e0b1d14
commit 2b91004129
2 changed files with 52 additions and 51 deletions

View File

@@ -15,6 +15,7 @@ import {
Archive, Archive,
Loader2, Loader2,
Sparkles, Sparkles,
RotateCcw,
} from 'lucide-react' } from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
@@ -128,6 +129,7 @@ function RouteComponent() {
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null) const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null)
const isInitialLoad = useRef(true)
const [showArchived, setShowArchived] = useState(false) const [showArchived, setShowArchived] = useState(false)
const [editingChatId, setEditingChatId] = useState<string | null>(null) const [editingChatId, setEditingChatId] = useState<string | null>(null)
const editableRef = useRef<HTMLSpanElement>(null) const editableRef = useRef<HTMLSpanElement>(null)
@@ -204,20 +206,20 @@ function RouteComponent() {
return messages return messages
}) })
}, [mensajesDelChat, activeChatId, availableFields]) }, [mensajesDelChat, activeChatId, availableFields])
const scrollToBottom = () => { const scrollToBottom = (behavior = 'smooth') => {
if (scrollRef.current) { if (scrollRef.current) {
// Buscamos el viewport interno del ScrollArea de Radix
const scrollContainer = scrollRef.current.querySelector( const scrollContainer = scrollRef.current.querySelector(
'[data-radix-scroll-area-viewport]', '[data-radix-scroll-area-viewport]',
) )
if (scrollContainer) { if (scrollContainer) {
scrollContainer.scrollTo({ scrollContainer.scrollTo({
top: scrollContainer.scrollHeight, top: scrollContainer.scrollHeight,
behavior: 'smooth', behavior: behavior, // 'instant' para carga inicial, 'smooth' para mensajes nuevos
}) })
} }
} }
} }
const { activeChats, archivedChats } = useMemo(() => { const { activeChats, archivedChats } = useMemo(() => {
const allChats = lastConversation || [] const allChats = lastConversation || []
return { return {
@@ -229,22 +231,22 @@ function RouteComponent() {
}, [lastConversation]) }, [lastConversation])
useEffect(() => { useEffect(() => {
console.log(mensajesDelChat) if (chatMessages.length > 0) {
if (isInitialLoad.current) {
scrollToBottom() // Si es el primer render con mensajes, vamos al final al instante
}, [chatMessages, isLoading]) scrollToBottom('instant')
isInitialLoad.current = false
/* useEffect(() => { } else {
// Verificamos cuáles campos de la lista "selectedFields" ya no están presentes en el texto del input // Si ya estaba cargado y llegan nuevos, hacemos el smooth
const camposActualizados = selectedFields.filter((field) => scrollToBottom('smooth')
input.includes(field.label),
)
// Solo actualizamos el estado si hubo un cambio real (para evitar bucles infinitos)
if (camposActualizados.length !== selectedFields.length) {
setSelectedFields(camposActualizados)
} }
}, [input, selectedFields]) */ }
}, [chatMessages])
// 2. Resetear el flag cuando cambies de chat activo
useEffect(() => {
isInitialLoad.current = true
}, [activeChatId])
useEffect(() => { useEffect(() => {
if (isLoadingConv || isSending) return if (isLoadingConv || isSending) return
@@ -508,27 +510,38 @@ function RouteComponent() {
</div> </div>
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<div className="space-y-1"> <div className="space-y-1 pr-2">
{' '}
{/* Agregamos un pr-2 para que el scrollbar no tape botones */}
{!showArchived ? ( {!showArchived ? (
activeChats.map((chat) => ( activeChats.map((chat) => (
<div <div
key={chat.id} key={chat.id}
onClick={() => setActiveChatId(chat.id)} onClick={() => setActiveChatId(chat.id)}
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-lg px-3 py-3 text-sm transition-colors ${ className={`group relative flex w-full items-center overflow-hidden rounded-lg px-3 py-3 text-sm transition-colors ${
activeChatId === chat.id activeChatId === chat.id
? 'bg-slate-100 font-medium text-slate-900' ? 'bg-slate-100 font-medium text-slate-900'
: 'text-slate-600 hover:bg-slate-50' : 'text-slate-600 hover:bg-slate-50'
}`} }`}
> >
{/* LADO IZQUIERDO: Icono + Texto con Tooltip */} {/* LADO IZQUIERDO: Icono + Texto */}
<div className="flex min-w-0 flex-1 items-center gap-3"> <div
className="flex min-w-0 flex-1 items-center gap-3 transition-all duration-200"
style={{
// Aplicamos la máscara solo cuando el mouse está encima para que se note el desvanecimiento
// donde aparecen los botones
maskImage:
'linear-gradient(to right, black 70%, transparent 95%)',
WebkitMaskImage:
'linear-gradient(to right, black 70%, transparent 95%)',
}}
>
{/* pr-12 reserva espacio para los botones absolutos */}
<FileText size={16} className="shrink-0 opacity-40" /> <FileText size={16} className="shrink-0 opacity-40" />
<TooltipProvider delayDuration={400}> <TooltipProvider delayDuration={400}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild className="min-w-0 flex-1">
{/* Este contenedor es el que obliga al span a truncarse */} <div className="min-w-0 flex-1">
<div className="max-w-[calc(100%-48px)] min-w-0 flex-1">
<span <span
ref={ ref={
editingChatId === chat.id ? editableRef : null editingChatId === chat.id ? editableRef : null
@@ -574,8 +587,6 @@ function RouteComponent() {
</span> </span>
</div> </div>
</TooltipTrigger> </TooltipTrigger>
{/* Tooltip: Solo aparece si no estás editando y el texto es largo */}
{editingChatId !== chat.id && ( {editingChatId !== chat.id && (
<TooltipContent <TooltipContent
side="right" side="right"
@@ -588,9 +599,9 @@ function RouteComponent() {
</TooltipProvider> </TooltipProvider>
</div> </div>
{/* LADO DERECHO: Acciones con shrink-0 para que no se muevan */} {/* LADO DERECHO: Acciones ABSOLUTAS */}
<div <div
className={`flex shrink-0 items-center gap-1 pl-2 opacity-0 transition-opacity group-hover:opacity-100 ${ className={`absolute top-1/2 right-2 z-20 flex -translate-y-1/2 items-center gap-1 rounded-md px-1 opacity-0 transition-opacity group-hover:opacity-100 ${
activeChatId === chat.id ? 'bg-slate-100' : 'bg-slate-50' activeChatId === chat.id ? 'bg-slate-100' : 'bg-slate-50'
}`} }`}
> >
@@ -614,7 +625,7 @@ function RouteComponent() {
</div> </div>
)) ))
) : ( ) : (
/* Sección de archivados */ /* Sección de archivados (Simplificada para mantener consistencia) */
<div className="animate-in fade-in slide-in-from-left-2 px-1"> <div className="animate-in fade-in slide-in-from-left-2 px-1">
<p className="mb-2 px-2 text-[10px] font-bold text-slate-400 uppercase"> <p className="mb-2 px-2 text-[10px] font-bold text-slate-400 uppercase">
Archivados Archivados
@@ -622,18 +633,18 @@ function RouteComponent() {
{archivedChats.map((chat) => ( {archivedChats.map((chat) => (
<div <div
key={chat.id} key={chat.id}
className="group relative mb-1 flex w-full items-center justify-between overflow-hidden rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400" className="group relative mb-1 flex w-full items-center overflow-hidden rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400"
> >
<div className="flex min-w-0 flex-1 items-center gap-3"> <div className="flex min-w-0 flex-1 items-center gap-3 pr-10">
<Archive size={14} className="shrink-0 opacity-30" /> <Archive size={14} className="shrink-0 opacity-30" />
<span className="block min-w-0 flex-1 truncate"> <span className="block truncate">
{chat.nombre || {chat.nombre ||
`Archivado ${chat.creado_en.split('T')[0]}`} `Archivado ${chat.creado_en.split('T')[0]}`}
</span> </span>
</div> </div>
<button <button
onClick={(e) => unarchiveChat(e, chat.id)} onClick={(e) => unarchiveChat(e, chat.id)}
className="ml-2 shrink-0 rounded bg-slate-50/80 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-teal-600" className="absolute top-1/2 right-2 shrink-0 -translate-y-1/2 rounded bg-slate-100 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-teal-600"
> >
<RotateCcw size={14} /> <RotateCcw size={14} />
</button> </button>

View File

@@ -166,30 +166,20 @@ function AsignaturaLayout() {
onSave={(val) => handleUpdateHeader('nombre', val)} onSave={(val) => handleUpdateHeader('nombre', val)}
/> />
</h1> </h1>
{
// console.log(headerData),
console.log(asignaturaApi.planes_estudio?.nombre)
}
<div className="flex flex-wrap gap-4 text-sm text-blue-200"> <div className="flex flex-wrap gap-4 text-sm text-blue-200">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<GraduationCap className="h-4 w-4 shrink-0" /> <GraduationCap className="h-4 w-4 shrink-0" />
Pertenece al plan:{' '}
<span className="text-blue-100"> <span className="text-blue-100">
{(asignaturaApi.planes_estudio?.datos as DatosPlan) {(asignaturaApi.planes_estudio as DatosPlan).nombre || ''}
.nombre || ''}
</span>
</span>
<span className="flex items-center gap-1">
<span className="text-blue-100">
{(asignaturaApi.planes_estudio?.datos as DatosPlan)
.nombre ?? ''}
</span> </span>
</span> </span>
</div> </div>
<p className="text-sm text-blue-300">
Pertenece al plan:{' '}
<span className="cursor-pointer underline">
{asignaturaApi.planes_estudio?.nombre}
</span>
</p>
</div> </div>
<div className="flex flex-col items-end gap-2 text-right"> <div className="flex flex-col items-end gap-2 text-right">