Se agrega editar por campos en ia y archivar chats

This commit is contained in:
2026-02-12 10:01:27 -06:00
parent 58d4ee8b6e
commit 2ec222694d

View File

@@ -1,10 +1,9 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/no-static-element-interactions */
import { createFileRoute, useRouterState } from '@tanstack/react-router' import { createFileRoute, useRouterState } from '@tanstack/react-router'
import { import {
Sparkles,
Send, Send,
Target, Target,
UserCheck,
Lightbulb, Lightbulb,
FileText, FileText,
GraduationCap, GraduationCap,
@@ -12,14 +11,14 @@ import {
Check, Check,
X, X,
MessageSquarePlus, MessageSquarePlus,
Trash2, Archive,
RotateCcw,
} from 'lucide-react' } from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone' import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA' import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Drawer, DrawerContent } from '@/components/ui/drawer' import { Drawer, DrawerContent } from '@/components/ui/drawer'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
@@ -99,6 +98,8 @@ function RouteComponent() {
const [chatHistory, setChatHistory] = useState([ const [chatHistory, setChatHistory] = useState([
{ id: '1', title: 'Chat inicial' }, { id: '1', title: 'Chat inicial' },
]) ])
const [showArchived, setShowArchived] = useState(false)
const [archivedHistory, setArchivedHistory] = useState<Array<any>>([])
const [allMessages, setAllMessages] = useState<{ [key: string]: Array<any> }>( const [allMessages, setAllMessages] = useState<{ [key: string]: Array<any> }>(
{ {
'1': [ '1': [
@@ -110,8 +111,6 @@ function RouteComponent() {
], ],
}, },
) )
const currentMessages = allMessages[activeChatId] || []
const createNewChat = () => { const createNewChat = () => {
const newId = Date.now().toString() const newId = Date.now().toString()
const newChat = { id: newId, title: `Nuevo chat ${chatHistory.length + 1}` } const newChat = { id: newId, title: `Nuevo chat ${chatHistory.length + 1}` }
@@ -130,15 +129,25 @@ function RouteComponent() {
setActiveChatId(newId) setActiveChatId(newId)
} }
const deleteChat = (e: React.MouseEvent, id: string) => { const archiveChat = (e: React.MouseEvent, id: string) => {
e.stopPropagation() // Evita que se seleccione el chat al borrarlo e.stopPropagation()
const newHistory = chatHistory.filter((chat) => chat.id !== id) const chatToArchive = chatHistory.find((chat) => chat.id === id)
setChatHistory(newHistory) if (chatToArchive) {
setArchivedHistory([chatToArchive, ...archivedHistory])
// Si borramos el chat activo, pasamos al primero disponible const newHistory = chatHistory.filter((chat) => chat.id !== id)
if (activeChatId === id && newHistory.length > 0) { setChatHistory(newHistory)
setActiveChatId(newHistory[0].id) 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))
} }
} }
@@ -225,11 +234,11 @@ function RouteComponent() {
.join('\n') .join('\n')
return ` return `
${userInput || 'Mejora los siguientes campos:'} ${userInput || 'Mejora los siguientes campos:'}
Campos a analizar: Campos a analizar:
${fieldsText} ${fieldsText}
`.trim() `.trim()
} }
const handleSend = async (promptOverride?: string) => { const handleSend = async (promptOverride?: string) => {
@@ -249,32 +258,49 @@ ${fieldsText}
setIsLoading(true) setIsLoading(true)
setTimeout(() => { setTimeout(() => {
const mockText = const suggestions = selectedFields.map((field) => ({
'Sugerencia generada basada en los campos seleccionados...' key: field.key,
label: field.label,
newValue: field.value,
}))
setMessages((prev) => [ setMessages((prev) => [
...prev, ...prev,
{ {
id: Date.now().toString(), id: Date.now().toString(),
role: 'assistant', role: 'assistant',
content: `He analizado ${selectedFields type: 'improvement-card',
.map((f) => f.label) content:
.join(', ')}. Aquí tienes una propuesta:\n\n${mockText}`, 'He analizado los campos seleccionados. Aquí tienes mis sugerencias de mejora:',
suggestions: suggestions,
}, },
]) ])
setPendingSuggestion({ text: mockText })
setIsLoading(false) setIsLoading(false)
}, 1200) }, 1200)
} }
return ( return (
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4"> <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="flex w-64 flex-col border-r pr-4">
<div className="mb-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">
Chats <h2 className="text-xs font-bold tracking-wider text-slate-500 uppercase">
</h2> 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 <Button
onClick={createNewChat} onClick={createNewChat}
variant="outline" variant="outline"
@@ -286,29 +312,61 @@ ${fieldsText}
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<div className="space-y-1"> <div className="space-y-1">
{chatHistory.map((chat) => ( {/* Lógica de renderizado condicional */}
// eslint-disable-next-line jsx-a11y/click-events-have-key-events {!showArchived ? (
<div // LISTA DE CHATS ACTIVOS
key={chat.id} chatHistory.map((chat) => (
onClick={() => setActiveChatId(chat.id)} <div
className={`group relative flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-3 text-sm transition-colors ${ key={chat.id}
activeChatId === chat.id onClick={() => setActiveChatId(chat.id)}
? 'bg-slate-100 font-medium text-slate-900' className={`group relative flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-3 text-sm transition-colors ${
: 'text-slate-600 hover:bg-slate-50' activeChatId === chat.id
}`} ? 'bg-slate-100 font-medium text-slate-900'
> : 'text-slate-600 hover:bg-slate-50'
<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 */}
<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"
> >
<Trash2 size={14} /> <FileText size={16} className="shrink-0 opacity-40" />
</button> <span className="truncate pr-8">{chat.title}</span>
<button
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"
>
<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>
))} )}
</div> </div>
</ScrollArea> </ScrollArea>
</div> </div>
@@ -335,32 +393,26 @@ ${fieldsText}
<div className="mx-auto max-w-3xl space-y-6 p-6"> <div className="mx-auto max-w-3xl space-y-6 p-6">
{messages.map((msg) => ( {messages.map((msg) => (
<div <div
key={msg.id} className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
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 <div
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`} className={`rounded-2xl p-3 text-sm whitespace-pre-wrap shadow-sm ${
msg.role === 'user'
? 'rounded-tr-none bg-teal-600 text-white'
: 'rounded-tl-none border bg-white text-slate-700'
}`}
> >
<div {msg.content}
className={`rounded-2xl p-3 text-sm whitespace-pre-wrap shadow-sm ${
msg.role === 'user' {msg.type === 'improvement-card' && (
? 'rounded-tr-none bg-teal-600 text-white' <ImprovementCard
: 'rounded-tl-none border bg-white text-slate-700' suggestions={msg.suggestions}
}`} onApply={(key, val) => {
> console.log(`Aplicando ${val} al campo ${key}`)
{msg.content} // Aquí llamarías a tu función de actualización de datos real
</div> }}
/>
)}
</div> </div>
</div> </div>
))} ))}
@@ -398,7 +450,7 @@ ${fieldsText}
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */} {/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
<div className="shrink-0 border-t bg-white p-4"> <div className="shrink-0 border-t bg-white p-4">
<div className="relative mx-auto max-w-4xl"> <div className="relative mx-auto max-w-4xl">
{/* MENÚ DE SUGERENCIAS FLOTANTE (Se mantiene igual) */} {/* MENÚ DE SUGERENCIAS FLOTANTE */}
{showSuggestions && ( {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="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"> <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 */} {/* 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"> <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 && ( {selectedFields.length > 0 && (
<div className="flex flex-wrap gap-2 px-2 pt-1"> <div className="flex flex-wrap gap-2 px-2 pt-1">
{selectedFields.map((field) => ( {selectedFields.map((field) => (
@@ -472,8 +524,7 @@ ${fieldsText}
</div> </div>
</div> </div>
</div> </div>
{/* PANEL LATERAL */}
{/* PANEL LATERAL (PRESETS) - SE MANTIENE COMO LO TENÍAS */}
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2"> <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"> <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 <Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
@@ -495,7 +546,6 @@ ${fieldsText}
))} ))}
</div> </div>
</div> </div>
<Drawer open={openIA} onOpenChange={setOpenIA}> <Drawer open={openIA} onOpenChange={setOpenIA}>
<DrawerContent className="fixed inset-0 h-screen w-screen max-w-none rounded-none"> <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"> <div className="flex items-center justify-between border-b p-4">
@@ -533,3 +583,66 @@ ${fieldsText}
</div> </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>
)
}