Que no haga scroll fix #193 #199
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user