Mejorar la responsividad de los chats fix #202

This commit is contained in:
2026-03-23 11:59:58 -06:00
parent 4c730fa0ab
commit 658c392f96
2 changed files with 408 additions and 157 deletions

View File

@@ -25,7 +25,12 @@ import type { IASugerencia } from '@/types/asignatura'
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Drawer, DrawerContent } from '@/components/ui/drawer'
import {
Drawer,
DrawerContent,
DrawerOverlay,
DrawerPortal,
} from '@/components/ui/drawer'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Textarea } from '@/components/ui/textarea'
import {
@@ -75,6 +80,7 @@ export function IAAsignaturaTab({
const [showSuggestions, setShowSuggestions] = useState(false)
const [isSending, setIsSending] = useState(false)
const scrollRef = useRef<HTMLDivElement>(null)
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
// --- DATA QUERIES ---
const { data: datosGenerales } = useSubject(asignaturaId)
@@ -355,10 +361,35 @@ export function IAAsignaturaTab({
]
return (
<div className="flex h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
{/* PANEL IZQUIERDO */}
<div className="flex w-64 flex-col border-r pr-4">
<div className="mb-4 flex items-center justify-between px-2">
<div className="flex h-full w-full overflow-hidden bg-white">
<div className="fixed top-0 z-40 flex w-full items-center justify-between border-b bg-white/80 p-2 backdrop-blur-md">
<Button
variant="ghost"
size="sm"
className="text-slate-600"
onClick={() => setIsSidebarOpen(true)}
>
<History size={18} className="mr-2" /> Historial
</Button>
<div className="flex flex-col items-center">
<span className="text-[9px] font-bold tracking-wider text-teal-600 uppercase">
Asistente
</span>
</div>
<Button
variant="ghost"
size="sm"
className="text-slate-600"
onClick={() => setOpenIA(true)} // O el drawer de acciones/referencias
>
<FileText size={18} className="mr-2 text-teal-600" /> Referencias
</Button>
</div>
{/* 1. PANEL IZQUIERDO (HISTORIAL) - Desktop */}
<aside className="hidden h-full w-64 shrink-0 flex-col border-r bg-white pr-4 md:flex">
<div className="mb-4 flex items-center justify-between px-2 pt-4">
<h2 className="flex items-center gap-2 text-xs font-bold text-slate-500 uppercase">
<History size={14} /> Historial
</h2>
@@ -378,25 +409,19 @@ export function IAAsignaturaTab({
<Button
onClick={() => {
setActiveChatId(undefined)
hasInitialSelected.current = true
setIsCreatingNewChat(true)
setInput('')
setSelectedFields([])
// 4. Opcional: Limpiar el caché de mensajes actual para que la pantalla se vea vacía al instante
queryClient.setQueryData(['subject-messages', undefined], [])
}}
variant="outline"
className="mb-4 w-full justify-start gap-2 border-dashed border-slate-300 hover:border-teal-500"
className="mb-4 w-full justify-start gap-2 border-dashed border-slate-300 text-slate-600 hover:border-teal-500 hover:bg-teal-50/50"
>
<MessageSquarePlus size={18} /> Nuevo Chat
</Button>
{/* PANEL IZQUIERDO - Cambios en ScrollArea y contenedor */}
<ScrollArea className="flex-1">
<div className="flex flex-col gap-1 pr-3">
{' '}
{/* Eliminado space-y-1 para mejor control con gap */}
{(showArchived ? archivedChats : activeChats).map((chat: any) => (
<div
key={chat.id}
@@ -507,31 +532,51 @@ export function IAAsignaturaTab({
))}
</div>
</ScrollArea>
</aside>
{/* 2. PANEL CENTRAL (CHAT) - EL RECUADRO ESTILIZADO */}
<main className="relative flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm md:m-2">
{/* Header Interno del Recuadro */}
<div className="flex shrink-0 items-center justify-between border-b bg-white p-3">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={() => setIsSidebarOpen(true)}
>
<History size={20} className="text-slate-500" />
</Button>
<div className="flex flex-col">
<span className="text-[10px] font-bold tracking-wider text-teal-600 uppercase">
Asistente Académico
</span>
<span className="text-[11px] text-slate-400">
Personalizado para tu asignatura
</span>
</div>
</div>
{/* PANEL CENTRAL */}
<div className="relative flex min-w-0 flex-[3] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm">
<div className="flex shrink-0 items-center justify-between border-b bg-white p-3">
<span className="text-[10px] font-bold tracking-wider text-slate-400 uppercase">
Asistente IA
</span>
<div className="flex gap-2">
<button
onClick={() => setOpenIA(true)}
className="flex items-center gap-2 rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium transition hover:bg-slate-200"
className="flex items-center gap-2 rounded-lg bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors hover:bg-slate-200"
>
<FileText size={14} className="text-slate-500" />
Referencias
<FileText size={14} />
<span className="xs:inline hidden">Referencias</span>
{totalReferencias > 0 && (
<span className="animate-in zoom-in flex h-4 min-w-[16px] items-center justify-center rounded-full bg-teal-600 px-1 text-[10px] text-white">
<span className="flex h-4 min-w-[16px] items-center justify-center rounded-full bg-teal-600 px-1 text-[10px] text-white">
{totalReferencias}
</span>
)}
</button>
</div>
</div>
{/* Área de Mensajes */}
<div className="relative min-h-0 flex-1">
<ScrollArea ref={scrollRef} className="h-full w-full">
<div className="mx-auto max-w-3xl space-y-8 p-6">
<div className="mx-auto max-w-3xl space-y-6 p-3 md:p-6">
{messages.map((msg) => (
<div
key={msg.id}
@@ -642,8 +687,9 @@ export function IAAsignaturaTab({
</ScrollArea>
</div>
{/* INPUT */}
<div className="shrink-0 border-t bg-white p-4">
{/* Input de Chat */}
<footer className="shrink-0 border-t bg-white p-3 md:p-4">
<div className="shrink-0 border-t bg-white p-2 md:p-4">
<div className="relative mx-auto max-w-4xl">
{showSuggestions && (
<div className="animate-in fade-in slide-in-from-bottom-2 absolute bottom-full left-0 z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl">
@@ -722,7 +768,8 @@ export function IAAsignaturaTab({
<Button
onClick={() => handleSend()}
disabled={
(!input.trim() && selectedFields.length === 0) || isSending
(!input.trim() && selectedFields.length === 0) ||
isSending
}
size="icon"
className="h-9 w-9 bg-teal-600 hover:bg-teal-700"
@@ -733,29 +780,33 @@ export function IAAsignaturaTab({
</div>
</div>
</div>
</div>
</footer>
</main>
{/* PANEL DERECHO ACCIONES */}
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
{/* 3. PANEL DERECHO (ATAJOS) */}
<aside className="hidden w-64 shrink-0 flex-col gap-4 overflow-y-auto p-4 lg:flex">
<h4 className="flex items-center gap-2 text-sm font-bold text-slate-800">
<Lightbulb size={18} className="text-orange-500" /> Atajos
<Lightbulb size={18} className="text-orange-500" /> Atajos Rápidos
</h4>
<div className="space-y-2">
{PRESETS.map((preset) => (
<button
key={preset.id}
onClick={() => handleSend(preset.prompt)}
className="group flex w-full items-center gap-3 rounded-xl border bg-white p-3 text-left text-sm transition-all hover:border-teal-500 hover:bg-teal-50"
className="group flex w-full items-center gap-3 rounded-xl border border-slate-100 bg-white p-3 text-left text-sm transition-all hover:border-teal-500 hover:shadow-sm"
>
<div className="rounded-lg bg-slate-100 p-2 group-hover:bg-teal-100 group-hover:text-teal-600">
<div className="rounded-lg bg-slate-50 p-2 group-hover:bg-teal-50 group-hover:text-teal-600">
<preset.icon size={16} />
</div>
<span className="font-medium text-slate-700">{preset.label}</span>
<span className="font-medium text-slate-600 group-hover:text-slate-900">
{preset.label}
</span>
</button>
))}
</div>
</div>
{/* --- DRAWER DE REFERENCIAS --- */}
</aside>
{/* DRAWERS (Referencias e Historial Móvil) */}
<Drawer open={openIA} onOpenChange={setOpenIA}>
<DrawerContent className="fixed inset-x-0 bottom-0 mx-auto mb-4 flex h-[80vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border bg-white shadow-2xl">
<div className="flex items-center justify-between border-b bg-slate-50/50 px-4 py-3">
@@ -790,6 +841,140 @@ export function IAAsignaturaTab({
</div>
</DrawerContent>
</Drawer>
<Drawer open={isSidebarOpen} onOpenChange={setIsSidebarOpen}>
<DrawerPortal>
<DrawerOverlay className="fixed inset-0 bg-black/40" />
<DrawerContent className="fixed right-0 bottom-0 left-0 h-[70vh] p-4 outline-none">
<div className="flex h-full flex-col overflow-hidden">
{/* Reutiliza aquí el componente de la lista de chats */}
<Button
onClick={() => {
setActiveChatId(undefined)
setIsCreatingNewChat(true)
setInput('')
setSelectedFields([])
queryClient.setQueryData(['subject-messages', undefined], [])
setIsSidebarOpen(false) // Cierra el drawer al crear nuevo
}}
variant="outline"
className="mb-6 w-full justify-center gap-2 border-dashed border-teal-500 bg-teal-50/50 text-teal-700 hover:bg-teal-100"
>
<MessageSquarePlus size={18} /> Nuevo Chat
</Button>
<h2 className="mb-4 font-bold">Historial de Chats</h2>
{(showArchived ? archivedChats : activeChats).map((chat: any) => (
<div
key={chat.id}
className={cn(
// Agregamos 'overflow-hidden' para que nada salga de este cuadro
'group relative flex w-full min-w-0 items-center justify-between gap-2 overflow-hidden rounded-lg px-3 py-2 text-sm transition-all',
activeChatId === chat.id
? 'bg-teal-50 text-teal-900'
: 'text-slate-600 hover:bg-slate-100',
)}
onDoubleClick={() => {
setEditingId(chat.id)
setTempName(chat.nombre || chat.titulo || 'Conversacion')
}}
>
{editingId === chat.id ? (
<div className="flex min-w-0 flex-1 items-center">
<input
autoFocus
className="w-full rounded border-none bg-white px-1 text-xs ring-1 ring-teal-400 outline-none"
value={tempName}
onChange={(e) => setTempName(e.target.value)}
onBlur={() => handleSaveName(chat.id)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveName(chat.id)
if (e.key === 'Escape') setEditingId(null)
}}
/>
</div>
) : (
<>
{/* CLAVE 2: 'truncate' y 'min-w-0' en el span para que ceda ante los botones */}
<span
onClick={() => setActiveChatId(chat.id)}
className="block max-w-[140px] min-w-0 flex-1 cursor-pointer truncate pr-1"
title={chat.nombre || chat.titulo}
>
{chat.nombre || chat.titulo || 'Conversación'}
</span>
{/* CLAVE 3: 'shrink-0' asegura que los botones NUNCA desaparezcan */}
<div
className={cn(
'z-10 flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100',
activeChatId === chat.id
? 'bg-teal-50'
: 'bg-slate-100',
)}
>
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation()
setEditingId(chat.id)
setTempName(chat.nombre || chat.titulo || '')
}}
className="rounded-md p-1 transition-colors hover:bg-slate-200 hover:text-teal-600"
>
<Edit2 size={14} />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="text-[10px]">
Editar nombre
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation()
const nuevoEstado =
chat.estado === 'ACTIVA'
? 'ARCHIVADA'
: 'ACTIVA'
updateStatus({
id: chat.id,
estado: nuevoEstado,
})
}}
className={cn(
'rounded-md p-1 transition-colors hover:bg-slate-200',
chat.estado === 'ACTIVA'
? 'hover:text-red-500'
: 'hover:text-teal-600',
)}
>
{chat.estado === 'ACTIVA' ? (
<Archive size={14} />
) : (
<History size={14} className="scale-x-[-1]" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="top" className="text-[10px]">
{chat.estado === 'ACTIVA'
? 'Archivar'
: 'Desarchivar'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</>
)}
</div>
))}
</div>
</DrawerContent>
</DrawerPortal>
</Drawer>
</div>
)
}

View File

@@ -139,6 +139,10 @@ function RouteComponent() {
null,
)
const [filterQuery, setFilterQuery] = useState('')
const [isHistoryOpen, setIsHistoryOpen] = useState(false)
const [isActionsOpen, setIsActionsOpen] = useState(false)
const availableFields = useMemo(() => {
const definicion = data?.estructuras_plan
?.definicion as EstructuraDefinicion
@@ -478,37 +482,38 @@ function RouteComponent() {
}
return (
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
{/* --- PANEL IZQUIERDO: HISTORIAL --- */}
<div className="flex w-64 flex-col border-r pr-4">
<div className="mb-4">
<div className="mb-4 flex items-center justify-between px-2">
<h2 className="text-xs font-bold tracking-wider text-slate-500 uppercase">
Chats
</h2>
{/* Botón de toggle archivados movido aquí arriba */}
<button
onClick={() => setShowArchived(!showArchived)}
className={`rounded-md p-1.5 transition-colors ${
showArchived
? 'bg-teal-50 text-teal-600'
: 'text-slate-400 hover:bg-slate-100'
}`}
title={showArchived ? 'Ver chats activos' : 'Ver archivados'}
>
<Archive size={16} />
</button>
</div>
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full flex-col gap-6 overflow-hidden p-4 md:flex-row">
{/* --- HEADER MÓVIL (Solo visible en < md) --- */}
<div className="flex items-center justify-between rounded-lg border bg-white p-2 md:hidden">
<Button
onClick={createNewChat}
variant="outline"
className="mb-4 w-full justify-start gap-2 border-slate-200 hover:bg-teal-50 hover:text-teal-700"
variant="ghost"
size="sm"
onClick={() => setIsHistoryOpen(true)}
>
<MessageSquarePlus size={18} /> Nuevo chat
<Archive size={18} className="mr-2" /> Historial
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setIsActionsOpen(true)}
>
<Lightbulb size={18} className="mr-2 text-orange-500" /> Acciones
</Button>
</div>
{/* --- PANEL IZQUIERDO: HISTORIAL (Escritorio) --- */}
<div className="hidden w-64 flex-col border-r pr-4 md:flex">
{/* ... (Tu código actual del historial de chats) ... */}
<h2 className="text-xs font-bold tracking-wider text-slate-500 uppercase">
Chats
</h2>
<Button
onClick={createNewChat}
variant="outline"
className="mt-2 mb-4 w-full justify-start gap-2"
>
<MessageSquarePlus size={18} /> Nuevo chat
</Button>
<ScrollArea className="flex-1">
<div className="space-y-1 pr-2">
{' '}
@@ -655,8 +660,9 @@ function RouteComponent() {
</div>
</ScrollArea>
</div>
{/* PANEL DE CHAT PRINCIPAL */}
<div className="relative flex min-w-0 flex-[3] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm">
{/* --- PANEL DE CHAT PRINCIPAL (Centro) --- */}
<div className="relative flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm md:flex-[3]">
{/* NUEVO: Barra superior de campos seleccionados */}
<div className="shrink-0 border-b bg-white p-3">
<div className="flex items-center justify-between">
@@ -931,8 +937,9 @@ function RouteComponent() {
</div>
</div>
</div>
{/* PANEL LATERAL */}
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
{/* --- PANEL LATERAL: ACCIONES RÁPIDAS (Escritorio) --- */}
<div className="hidden flex-[1] flex-col gap-4 overflow-y-auto pr-2 md:flex">
<h4 className="flex items-center gap-2 text-left text-sm font-bold text-slate-800">
<Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
</h4>
@@ -953,6 +960,65 @@ function RouteComponent() {
))}
</div>
</div>
{/* --- DRAWER: HISTORIAL (Móvil) --- */}
<Drawer open={isHistoryOpen} onOpenChange={setIsHistoryOpen}>
<DrawerContent className="h-[80vh] p-4">
<Button
onClick={() => {
createNewChat()
setIsHistoryOpen(false)
}}
className="mb-4 w-full bg-teal-600 text-white"
>
<MessageSquarePlus size={18} className="mr-2" /> Nuevo Chat
</Button>
<ScrollArea className="flex-1">
{/* Reutiliza aquí el mapeo de chats que tienes en el panel izquierdo */}
<p className="mb-4 text-xs font-bold text-slate-400 uppercase">
Historial Reciente
</p>
{activeChats.map((chat) => (
<div
key={chat.id}
onClick={() => {
setActiveChatId(chat.id)
setIsHistoryOpen(false)
}}
className="border-b p-3 text-sm"
>
{chat.nombre || 'Chat sin nombre'}
</div>
))}
</ScrollArea>
</DrawerContent>
</Drawer>
{/* --- DRAWER: ACCIONES RÁPIDAS (Móvil) --- */}
<Drawer open={isActionsOpen} onOpenChange={setIsActionsOpen}>
<DrawerContent className="h-[60vh] p-4">
<h4 className="mb-4 flex items-center gap-2 font-bold">
<Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
</h4>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{PRESETS.map((preset) => (
<button
key={preset.id}
onClick={() => {
handleSend(preset.prompt)
setIsActionsOpen(false)
}}
className="flex items-center gap-3 rounded-xl border p-4 text-left text-sm"
>
<preset.icon size={16} />
<span>{preset.label}</span>
</button>
))}
</div>
</DrawerContent>
</Drawer>
{/* Tu Drawer de Referencias IA se queda igual */}
<Drawer open={openIA} onOpenChange={setOpenIA}>
<DrawerContent className="fixed inset-x-0 bottom-0 mx-auto mb-4 flex h-[80vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border bg-white shadow-2xl">
{/* Cabecera más compacta */}