Merge pull request 'Archivado de chats y editar por campos de ia' (#94) from issue/90-historial-de-chats-archivado into main

Reviewed-on: #94
This commit was merged in pull request #94.
This commit is contained in:
2026-02-12 16:02:51 +00:00

View File

@@ -1,10 +1,9 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import { createFileRoute, useRouterState } from '@tanstack/react-router'
import {
Sparkles,
Send,
Target,
UserCheck,
Lightbulb,
FileText,
GraduationCap,
@@ -12,14 +11,14 @@ import {
Check,
X,
MessageSquarePlus,
Trash2,
Archive,
RotateCcw,
} from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from 'react'
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
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 { ScrollArea } from '@/components/ui/scroll-area'
@@ -99,6 +98,8 @@ function RouteComponent() {
const [chatHistory, setChatHistory] = useState([
{ id: '1', title: 'Chat inicial' },
])
const [showArchived, setShowArchived] = useState(false)
const [archivedHistory, setArchivedHistory] = useState<Array<any>>([])
const [allMessages, setAllMessages] = useState<{ [key: string]: Array<any> }>(
{
'1': [
@@ -110,8 +111,6 @@ function RouteComponent() {
],
},
)
const currentMessages = allMessages[activeChatId] || []
const createNewChat = () => {
const newId = Date.now().toString()
const newChat = { id: newId, title: `Nuevo chat ${chatHistory.length + 1}` }
@@ -130,17 +129,27 @@ function RouteComponent() {
setActiveChatId(newId)
}
const deleteChat = (e: React.MouseEvent, id: string) => {
e.stopPropagation() // Evita que se seleccione el chat al borrarlo
const archiveChat = (e: React.MouseEvent, id: string) => {
e.stopPropagation()
const chatToArchive = chatHistory.find((chat) => chat.id === id)
if (chatToArchive) {
setArchivedHistory([chatToArchive, ...archivedHistory])
const newHistory = chatHistory.filter((chat) => chat.id !== id)
setChatHistory(newHistory)
// Si borramos el chat activo, pasamos al primero disponible
if (activeChatId === id && newHistory.length > 0) {
setActiveChatId(newHistory[0].id)
}
}
}
const unarchiveChat = (e: React.MouseEvent, id: string) => {
e.stopPropagation()
const chatToRestore = archivedHistory.find((chat) => chat.id === id)
if (chatToRestore) {
setChatHistory([chatToRestore, ...chatHistory])
setArchivedHistory(archivedHistory.filter((chat) => chat.id !== id))
}
}
// 1. Transformar datos de la API para el menú de selección
const availableFields = useMemo(() => {
@@ -225,11 +234,11 @@ function RouteComponent() {
.join('\n')
return `
${userInput || 'Mejora los siguientes campos:'}
${userInput || 'Mejora los siguientes campos:'}
Campos a analizar:
${fieldsText}
`.trim()
Campos a analizar:
${fieldsText}
`.trim()
}
const handleSend = async (promptOverride?: string) => {
@@ -249,32 +258,49 @@ ${fieldsText}
setIsLoading(true)
setTimeout(() => {
const mockText =
'Sugerencia generada basada en los campos seleccionados...'
const suggestions = selectedFields.map((field) => ({
key: field.key,
label: field.label,
newValue: field.value,
}))
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: 'assistant',
content: `He analizado ${selectedFields
.map((f) => f.label)
.join(', ')}. Aquí tienes una propuesta:\n\n${mockText}`,
type: 'improvement-card',
content:
'He analizado los campos seleccionados. Aquí tienes mis sugerencias de mejora:',
suggestions: suggestions,
},
])
setPendingSuggestion({ text: mockText })
setIsLoading(false)
}, 1200)
}
return (
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
{/* --- PANEL IZQUIERDO: HISTORIAL (NUEVO) --- */}
{/* --- PANEL IZQUIERDO: HISTORIAL --- */}
<div className="flex w-64 flex-col border-r pr-4">
<div className="mb-4">
<h2 className="mb-4 px-2 text-xs font-bold tracking-wider text-slate-500 uppercase">
<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>
<Button
onClick={createNewChat}
variant="outline"
@@ -286,8 +312,10 @@ ${fieldsText}
<ScrollArea className="flex-1">
<div className="space-y-1">
{chatHistory.map((chat) => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
{/* Lógica de renderizado condicional */}
{!showArchived ? (
// LISTA DE CHATS ACTIVOS
chatHistory.map((chat) => (
<div
key={chat.id}
onClick={() => setActiveChatId(chat.id)}
@@ -298,17 +326,47 @@ ${fieldsText}
}`}
>
<FileText size={16} className="shrink-0 opacity-40" />
<span className="truncate pr-6">{chat.title}</span>
{/* Botón de borrar que aparece al hacer hover */}
<span className="truncate pr-8">{chat.title}</span>
<button
onClick={(e) => deleteChat(e, chat.id)}
className="absolute right-2 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-600"
onClick={(e) => archiveChat(e, chat.id)}
className="absolute right-2 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-amber-600"
title="Archivar"
>
<Trash2 size={14} />
<Archive size={14} />
</button>
</div>
))
) : (
// LISTA DE CHATS ARCHIVADOS
<div className="animate-in fade-in slide-in-from-left-2">
<p className="mb-2 px-2 text-[10px] font-bold text-slate-400 uppercase">
Archivados
</p>
{archivedHistory.map((chat) => (
<div
key={chat.id}
className="group relative mb-1 flex w-full items-center gap-3 rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400"
>
<Archive size={14} className="shrink-0 opacity-30" />
<span className="truncate pr-8">{chat.title}</span>
<button
onClick={(e) => unarchiveChat(e, chat.id)}
className="absolute right-2 p-1 opacity-0 group-hover:opacity-100 hover:text-teal-600"
title="Desarchivar"
>
<RotateCcw size={14} />
</button>
</div>
))}
{archivedHistory.length === 0 && (
<div className="px-2 py-4 text-center">
<p className="text-xs text-slate-400 italic">
No hay archivados
</p>
</div>
)}
</div>
)}
</div>
</ScrollArea>
</div>
@@ -334,21 +392,6 @@ ${fieldsText}
<ScrollArea ref={scrollRef} className="h-full w-full">
<div className="mx-auto max-w-3xl space-y-6 p-6">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} items-start gap-3`}
>
<Avatar
className={`h-8 w-8 shrink-0 border ${msg.role === 'assistant' ? 'bg-teal-50' : 'bg-slate-200'}`}
>
<AvatarFallback className="text-[10px]">
{msg.role === 'assistant' ? (
<Sparkles size={14} className="text-teal-600" />
) : (
<UserCheck size={14} />
)}
</AvatarFallback>
</Avatar>
<div
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
>
@@ -360,7 +403,16 @@ ${fieldsText}
}`}
>
{msg.content}
</div>
{msg.type === 'improvement-card' && (
<ImprovementCard
suggestions={msg.suggestions}
onApply={(key, val) => {
console.log(`Aplicando ${val} al campo ${key}`)
// Aquí llamarías a tu función de actualización de datos real
}}
/>
)}
</div>
</div>
))}
@@ -398,7 +450,7 @@ ${fieldsText}
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
<div className="shrink-0 border-t bg-white p-4">
<div className="relative mx-auto max-w-4xl">
{/* MENÚ DE SUGERENCIAS FLOTANTE (Se mantiene igual) */}
{/* MENÚ DE SUGERENCIAS FLOTANTE */}
{showSuggestions && (
<div className="animate-in slide-in-from-bottom-2 absolute bottom-full z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl">
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold tracking-wider text-slate-500 uppercase">
@@ -425,7 +477,7 @@ ${fieldsText}
{/* CONTENEDOR DEL INPUT TRANSFORMADO */}
<div className="flex flex-col gap-2 rounded-xl border bg-slate-50 p-2 transition-all focus-within:bg-white focus-within:ring-1 focus-within:ring-teal-500">
{/* 1. Visualización de campos dentro del input (Tags) */}
{/* 1. Visualización de campos dentro del input ) */}
{selectedFields.length > 0 && (
<div className="flex flex-wrap gap-2 px-2 pt-1">
{selectedFields.map((field) => (
@@ -472,8 +524,7 @@ ${fieldsText}
</div>
</div>
</div>
{/* PANEL LATERAL (PRESETS) - SE MANTIENE COMO LO TENÍAS */}
{/* PANEL LATERAL */}
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
<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
@@ -495,7 +546,6 @@ ${fieldsText}
))}
</div>
</div>
<Drawer open={openIA} onOpenChange={setOpenIA}>
<DrawerContent className="fixed inset-0 h-screen w-screen max-w-none rounded-none">
<div className="flex items-center justify-between border-b p-4">
@@ -533,3 +583,66 @@ ${fieldsText}
</div>
)
}
const ImprovementCard = ({
suggestions,
onApply,
}: {
suggestions: Array<any>
onApply: (key: string, value: string) => void
}) => {
// Estado para rastrear qué campos han sido aplicados
const [appliedFields, setAppliedFields] = useState<Array<string>>([])
const handleApply = (key: string, value: string) => {
onApply(key, value)
setAppliedFields((prev) => [...prev, key])
}
return (
<div className="mt-2 flex w-full flex-col gap-4">
{suggestions.map((sug) => {
const isApplied = appliedFields.includes(sug.key)
return (
<div
key={sug.key}
className="rounded-2xl border border-slate-100 bg-white p-5 shadow-sm"
>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-bold text-slate-900">{sug.label}</h3>
<Button
size="sm"
onClick={() => handleApply(sug.key, sug.newValue)}
disabled={isApplied}
className={`h-8 rounded-full px-4 text-xs transition-all ${
isApplied
? 'cursor-not-allowed bg-slate-100 text-slate-400'
: 'bg-[#00a189] text-white hover:bg-[#008f7a]'
}`}
>
{isApplied ? (
<span className="flex items-center gap-1">
<Check size={12} /> Aplicado
</span>
) : (
'Aplicar mejora'
)}
</Button>
</div>
<div
className={`rounded-xl border p-3 text-sm transition-colors duration-300 ${
isApplied
? 'border-[#ccfbf1] bg-[#f0fdfa] text-slate-700'
: 'border-slate-200 bg-slate-50 text-slate-500'
}`}
>
{sug.newValue}
</div>
</div>
)
})}
</div>
)
}