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:
@@ -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,17 +129,27 @@ 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 chatToArchive = chatHistory.find((chat) => chat.id === id)
|
||||||
|
if (chatToArchive) {
|
||||||
|
setArchivedHistory([chatToArchive, ...archivedHistory])
|
||||||
const newHistory = chatHistory.filter((chat) => chat.id !== id)
|
const newHistory = chatHistory.filter((chat) => chat.id !== id)
|
||||||
setChatHistory(newHistory)
|
setChatHistory(newHistory)
|
||||||
|
|
||||||
// Si borramos el chat activo, pasamos al primero disponible
|
|
||||||
if (activeChatId === id && newHistory.length > 0) {
|
if (activeChatId === id && newHistory.length > 0) {
|
||||||
setActiveChatId(newHistory[0].id)
|
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
|
// 1. Transformar datos de la API para el menú de selección
|
||||||
const availableFields = useMemo(() => {
|
const availableFields = useMemo(() => {
|
||||||
@@ -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">
|
||||||
|
<h2 className="text-xs font-bold tracking-wider text-slate-500 uppercase">
|
||||||
Chats
|
Chats
|
||||||
</h2>
|
</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,8 +312,10 @@ ${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 ? (
|
||||||
|
// LISTA DE CHATS ACTIVOS
|
||||||
|
chatHistory.map((chat) => (
|
||||||
<div
|
<div
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
onClick={() => setActiveChatId(chat.id)}
|
onClick={() => setActiveChatId(chat.id)}
|
||||||
@@ -298,17 +326,47 @@ ${fieldsText}
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FileText size={16} className="shrink-0 opacity-40" />
|
<FileText size={16} className="shrink-0 opacity-40" />
|
||||||
<span className="truncate pr-6">{chat.title}</span>
|
<span className="truncate pr-8">{chat.title}</span>
|
||||||
|
|
||||||
{/* Botón de borrar que aparece al hacer hover */}
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => deleteChat(e, chat.id)}
|
onClick={(e) => archiveChat(e, chat.id)}
|
||||||
className="absolute right-2 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-600"
|
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>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
@@ -334,21 +392,6 @@ ${fieldsText}
|
|||||||
<ScrollArea ref={scrollRef} className="h-full w-full">
|
<ScrollArea ref={scrollRef} className="h-full w-full">
|
||||||
<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
|
|
||||||
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
|
<div
|
||||||
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
||||||
>
|
>
|
||||||
@@ -360,7 +403,16 @@ ${fieldsText}
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{msg.content}
|
{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>
|
||||||
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user