Se quitó teal y otros estilos hardcoded de la página
This commit is contained in:
@@ -115,7 +115,7 @@ function RouteComponent() {
|
||||
const { data: lastConversation, isLoading: isLoadingConv } =
|
||||
useConversationByPlan(planId)
|
||||
const { data: mensajesDelChat, isLoading: isLoadingMessages } =
|
||||
useMessagesByChat(activeChatId ?? null) // Si es undefined, pasa null
|
||||
useMessagesByChat(activeChatId ?? null)
|
||||
const [selectedArchivoIds, setSelectedArchivoIds] = useState<Array<string>>(
|
||||
[],
|
||||
)
|
||||
@@ -155,8 +155,7 @@ function RouteComponent() {
|
||||
const definicion = data?.estructuras_plan
|
||||
?.definicion as EstructuraDefinicion
|
||||
|
||||
// Encadenamiento opcional para evitar errores si data es null
|
||||
if (!definicion.properties) return []
|
||||
if (!definicion?.properties) return []
|
||||
|
||||
return Object.entries(definicion.properties).map(([key, value]) => ({
|
||||
key,
|
||||
@@ -169,28 +168,24 @@ function RouteComponent() {
|
||||
return availableFields.filter(
|
||||
(field) =>
|
||||
field.label.toLowerCase().includes(filterQuery.toLowerCase()) &&
|
||||
!selectedFields.some((s) => s.key === field.key), // No mostrar ya seleccionados
|
||||
!selectedFields.some((s) => s.key === field.key),
|
||||
)
|
||||
}, [availableFields, filterQuery, selectedFields])
|
||||
|
||||
const chatMessages = useMemo(() => {
|
||||
if (!activeChatId || !mensajesDelChat) return []
|
||||
|
||||
// flatMap nos permite devolver 2 elementos (pregunta y respuesta) por cada registro de la BD
|
||||
return mensajesDelChat.flatMap((msg: any) => {
|
||||
const messages = []
|
||||
|
||||
// 1. Mensaje del Usuario
|
||||
messages.push({
|
||||
id: `${msg.id}-user`,
|
||||
role: 'user',
|
||||
content: msg.mensaje,
|
||||
selectedFields: msg.campos || [], // Aquí están tus campos
|
||||
selectedFields: msg.campos || [],
|
||||
})
|
||||
|
||||
// 2. Mensaje del Asistente (si hay respuesta)
|
||||
if (msg.respuesta) {
|
||||
// Extraemos las recomendaciones de la nueva estructura: msg.propuesta.recommendations
|
||||
const rawRecommendations = msg.propuesta?.recommendations || []
|
||||
|
||||
messages.push({
|
||||
@@ -219,8 +214,6 @@ function RouteComponent() {
|
||||
})
|
||||
}, [mensajesDelChat, activeChatId, availableFields])
|
||||
|
||||
// En tu componente principal (el padre)
|
||||
|
||||
const handleApplyMultiple = async (
|
||||
sugerencias: Array<any>,
|
||||
dbMessageId: string,
|
||||
@@ -258,15 +251,12 @@ function RouteComponent() {
|
||||
conversacionId: dbMessageId,
|
||||
campoAfectado: sug.key,
|
||||
})
|
||||
console.log(sug.key)
|
||||
|
||||
removeSelectedField(sug.key)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Error al marcar aplicada la sugerencia: ${sug.key}`,
|
||||
err,
|
||||
)
|
||||
// Si una falla, el bucle sigue con las demás
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,17 +325,14 @@ function RouteComponent() {
|
||||
useEffect(() => {
|
||||
if (chatMessages.length > 0) {
|
||||
if (isInitialLoad.current) {
|
||||
// Si es el primer render con mensajes, vamos al final al instante
|
||||
scrollToBottom('instant')
|
||||
isInitialLoad.current = false
|
||||
} else {
|
||||
// Si ya estaba cargado y llegan nuevos, hacemos el smooth
|
||||
scrollToBottom('smooth')
|
||||
}
|
||||
}
|
||||
}, [chatMessages])
|
||||
|
||||
// 2. Resetear el flag cuando cambies de chat activo
|
||||
useEffect(() => {
|
||||
isInitialLoad.current = true
|
||||
}, [activeChatId])
|
||||
@@ -358,14 +345,12 @@ function RouteComponent() {
|
||||
)
|
||||
const isCreationMode = messages.length === 1 && messages[0].id === 'welcome'
|
||||
|
||||
// 1. Si el chat que teníamos seleccionado ya no existe (ej. se archivó)
|
||||
if (activeChatId && !currentChatExists && !isCreationMode) {
|
||||
setActiveChatId(undefined)
|
||||
setMessages([])
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Auto-selección inicial: Solo si no hay ID, no estamos creando y hay chats
|
||||
if (
|
||||
!activeChatId &&
|
||||
activeChats.length > 0 &&
|
||||
@@ -399,7 +384,7 @@ function RouteComponent() {
|
||||
}, [availableFields, routerState.location.state])
|
||||
|
||||
const createNewChat = () => {
|
||||
setActiveChatId(undefined) // Al ser undefined, el próximo handleSend creará uno nuevo
|
||||
setActiveChatId(undefined)
|
||||
setMessages([
|
||||
{
|
||||
id: 'welcome',
|
||||
@@ -408,7 +393,6 @@ function RouteComponent() {
|
||||
},
|
||||
])
|
||||
setInput('')
|
||||
// setSelectedFields([])
|
||||
}
|
||||
|
||||
const archiveChat = (e: React.MouseEvent, id: string) => {
|
||||
@@ -450,16 +434,15 @@ function RouteComponent() {
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const val = e.target.value
|
||||
const cursorPosition = e.target.selectionStart // Dónde está escribiendo el usuario
|
||||
const cursorPosition = e.target.selectionStart
|
||||
setInput(val)
|
||||
|
||||
// Busca un ":" seguido de letras justo antes del cursor
|
||||
const textBeforeCursor = val.slice(0, cursorPosition)
|
||||
const match = textBeforeCursor.match(/:(\w*)$/)
|
||||
|
||||
if (match) {
|
||||
setShowSuggestions(true)
|
||||
setFilterQuery(match[1]) // Esto es lo que se usa para el filtrado
|
||||
setFilterQuery(match[1])
|
||||
} else {
|
||||
setShowSuggestions(false)
|
||||
setFilterQuery('')
|
||||
@@ -470,40 +453,31 @@ function RouteComponent() {
|
||||
input: string,
|
||||
fields: Array<SelectedField>,
|
||||
) => {
|
||||
// 1. Limpiamos cualquier rastro anterior de la etiqueta (por si acaso)
|
||||
// Esta regex ahora también limpia si el texto termina de forma natural
|
||||
const cleaned = input.replace(/[:\s]+[^:]*$/, '').trim()
|
||||
|
||||
if (fields.length === 0) return cleaned
|
||||
|
||||
const fieldLabels = fields.map((f) => f.label).join(', ')
|
||||
|
||||
// 2. Devolvemos un formato natural: "Mejora este campo: Nombre del Campo"
|
||||
return `${cleaned}: ${fieldLabels}`
|
||||
}
|
||||
|
||||
const toggleField = (field: SelectedField) => {
|
||||
// 1. Lo agregamos a la lista de "SelectedFields" (para que la IA sepa qué procesar)
|
||||
setSelectedFields((prev) => {
|
||||
const isSelected = prev.find((f) => f.key === field.key)
|
||||
return isSelected ? prev : [...prev, field]
|
||||
})
|
||||
|
||||
// 2. Insertamos el nombre del campo en el texto exactamente donde estaba el ":"
|
||||
setInput((prev) => {
|
||||
// Reemplaza el último ":" y cualquier texto de filtro por el label del campo
|
||||
const nuevoTexto = prev.replace(/:(\w*)$/, field.label)
|
||||
return nuevoTexto + ' ' // Añadimos un espacio para que el usuario siga escribiendo
|
||||
return nuevoTexto + ' '
|
||||
})
|
||||
|
||||
// 3. Limpiamos estados de búsqueda
|
||||
setShowSuggestions(false)
|
||||
setFilterQuery('')
|
||||
}
|
||||
|
||||
const buildPrompt = (userInput: string, fields: Array<SelectedField>) => {
|
||||
if (fields.length === 0) return userInput
|
||||
|
||||
return ` ${userInput}`
|
||||
}
|
||||
|
||||
@@ -516,7 +490,6 @@ function RouteComponent() {
|
||||
setIsSending(true)
|
||||
setOptimisticMessage(finalContent)
|
||||
setInput('')
|
||||
// setSelectedFields([])
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
@@ -535,7 +508,6 @@ function RouteComponent() {
|
||||
setActiveChatId(response.conversacionId)
|
||||
}
|
||||
|
||||
// ESPERAMOS a que la caché se actualice antes de quitar el "isSending"
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['conversation-by-plan', planId],
|
||||
@@ -548,19 +520,15 @@ function RouteComponent() {
|
||||
console.error('Error:', error)
|
||||
setOptimisticMessage(null)
|
||||
} finally {
|
||||
// Solo ahora quitamos los indicadores de carga
|
||||
setIsSending(false)
|
||||
// setOptimisticMessage(null)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSyncing || !mensajesDelChat || mensajesDelChat.length === 0) return
|
||||
|
||||
// Forzamos el tipo a 'any' o a tu interfaz de mensaje para saltarnos la unión de tipos compleja
|
||||
const ultimoMensajeDB = mensajesDelChat[mensajesDelChat.length - 1] as any
|
||||
|
||||
// Ahora la validación es directa y no debería dar avisos de "unnecessary"
|
||||
if (ultimoMensajeDB?.respuesta) {
|
||||
setIsSyncing(false)
|
||||
setOptimisticMessage(null)
|
||||
@@ -576,14 +544,13 @@ function RouteComponent() {
|
||||
}, [selectedArchivoIds, selectedRepositorioIds, uploadedFiles])
|
||||
|
||||
const removeSelectedField = (fieldKey: string) => {
|
||||
console.log(fieldKey)
|
||||
setSelectedFields((prev) => prev.filter((f) => f.key !== fieldKey))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-80px)] w-full flex-col gap-4 pb-1 md:h-[calc(100vh-160px)] md:max-h-[calc(100vh-160px)] md:flex-row md:overflow-hidden">
|
||||
{/* --- HEADER MÓVIL (Solo visible en < md) --- */}
|
||||
<div className="flex shrink-0 items-center justify-between rounded-lg border bg-white p-2 shadow-sm md:hidden">
|
||||
{/* --- HEADER MÓVIL --- */}
|
||||
<div className="bg-background flex shrink-0 items-center justify-between rounded-lg border p-2 shadow-sm md:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -596,14 +563,13 @@ function RouteComponent() {
|
||||
size="sm"
|
||||
onClick={() => setIsActionsOpen(true)}
|
||||
>
|
||||
<Lightbulb size={18} className="mr-2 text-orange-500" /> Acciones
|
||||
<Lightbulb size={18} className="text-primary mr-2" /> Acciones
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* --- PANEL IZQUIERDO: HISTORIAL (Escritorio) --- */}
|
||||
{/* --- PANEL IZQUIERDO: HISTORIAL --- */}
|
||||
<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">
|
||||
<h2 className="text-muted-foreground text-xs font-bold tracking-wider uppercase">
|
||||
Chats
|
||||
</h2>
|
||||
<Button
|
||||
@@ -615,8 +581,6 @@ function RouteComponent() {
|
||||
</Button>
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-1 pr-2">
|
||||
{' '}
|
||||
{/* Agregamos un pr-2 para que el scrollbar no tape botones */}
|
||||
{!showArchived ? (
|
||||
activeChats.map((chat) => (
|
||||
<div
|
||||
@@ -624,23 +588,19 @@ function RouteComponent() {
|
||||
onClick={() => setActiveChatId(chat.id)}
|
||||
className={`group relative flex w-full items-center overflow-hidden rounded-lg px-3 py-3 text-sm transition-colors ${
|
||||
activeChatId === chat.id
|
||||
? 'bg-slate-100 font-medium text-slate-900'
|
||||
: 'text-slate-600 hover:bg-slate-50'
|
||||
? 'bg-accent text-foreground font-medium'
|
||||
: 'text-muted-foreground hover:bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
{/* LADO IZQUIERDO: Icono + Texto */}
|
||||
<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" />
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<Tooltip>
|
||||
@@ -654,7 +614,7 @@ function RouteComponent() {
|
||||
suppressContentEditableWarning={true}
|
||||
className={`block truncate outline-none ${
|
||||
editingChatId === chat.id
|
||||
? 'max-h-20 min-w-[100px] cursor-text overflow-y-auto rounded bg-white px-1 break-all shadow-sm ring-1 ring-teal-500'
|
||||
? 'bg-background ring-primary max-h-20 min-w-[100px] cursor-text overflow-y-auto rounded px-1 break-all shadow-sm ring-1'
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
onDoubleClick={(e) => {
|
||||
@@ -703,10 +663,9 @@ function RouteComponent() {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{/* LADO DERECHO: Acciones ABSOLUTAS */}
|
||||
<div
|
||||
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-accent' : 'bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
@@ -715,13 +674,13 @@ function RouteComponent() {
|
||||
setEditingChatId(chat.id)
|
||||
setTimeout(() => editableRef.current?.focus(), 50)
|
||||
}}
|
||||
className="rounded-md p-1 text-slate-400 transition-colors hover:text-teal-600"
|
||||
className="text-muted-foreground hover:text-primary rounded-md p-1 transition-colors"
|
||||
>
|
||||
<Send size={12} className="rotate-45" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => archiveChat(e, chat.id)}
|
||||
className="rounded-md p-1 text-slate-400 transition-colors hover:text-amber-600"
|
||||
className="text-muted-foreground hover:text-destructive rounded-md p-1 transition-colors"
|
||||
>
|
||||
<Archive size={14} />
|
||||
</button>
|
||||
@@ -729,15 +688,14 @@ function RouteComponent() {
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
/* Sección de archivados (Simplificada para mantener consistencia) */
|
||||
<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="text-muted-foreground mb-2 px-2 text-[10px] font-bold uppercase">
|
||||
Archivados
|
||||
</p>
|
||||
{archivedChats.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
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"
|
||||
className="bg-muted/50 text-muted-foreground group relative mb-1 flex w-full items-center overflow-hidden rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3 pr-10">
|
||||
<Archive size={14} className="shrink-0 opacity-30" />
|
||||
@@ -748,7 +706,7 @@ function RouteComponent() {
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => unarchiveChat(e, chat.id)}
|
||||
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"
|
||||
className="bg-accent hover:text-primary absolute top-1/2 right-2 shrink-0 -translate-y-1/2 rounded p-1 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
@@ -760,22 +718,21 @@ function RouteComponent() {
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* --- 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:h-full md:flex-[3]">
|
||||
{/* NUEVO: Barra superior de campos seleccionados */}
|
||||
<div className="z-10 shrink-0 border-b bg-white p-3">
|
||||
{/* --- PANEL DE CHAT PRINCIPAL --- */}
|
||||
<div className="border-border/60 bg-muted/30 relative flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl border shadow-sm md:h-full md:flex-[3]">
|
||||
<div className="bg-background z-10 shrink-0 border-b p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase">
|
||||
<span className="text-muted-foreground text-[10px] font-bold uppercase">
|
||||
Mejorar con IA
|
||||
</span>
|
||||
<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="bg-secondary text-secondary-foreground hover:bg-secondary/80 flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium transition"
|
||||
>
|
||||
<Archive size={14} className="text-slate-500" />
|
||||
<Archive size={14} className="opacity-70" />
|
||||
Referencias
|
||||
{totalReferencias > 0 && (
|
||||
<span className="flex h-4 min-w-[16px] items-center justify-center rounded-full bg-teal-600 px-1 text-[10px] text-white">
|
||||
<span className="bg-primary text-primary-foreground flex h-4 min-w-[16px] items-center justify-center rounded-full px-1 text-[10px]">
|
||||
{totalReferencias}
|
||||
</span>
|
||||
)}
|
||||
@@ -783,7 +740,6 @@ function RouteComponent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CONTENIDO DEL CHAT */}
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<ScrollArea ref={scrollRef} className="h-full w-full">
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-6 p-6">
|
||||
@@ -793,12 +749,12 @@ function RouteComponent() {
|
||||
<div className="flex h-[400px] flex-col items-center justify-center text-center opacity-40">
|
||||
<MessageSquarePlus
|
||||
size={48}
|
||||
className="mb-4 text-slate-300"
|
||||
className="text-muted-foreground/50 mb-4"
|
||||
/>
|
||||
<h3 className="text-lg font-medium text-slate-900">
|
||||
<h3 className="text-foreground text-lg font-medium">
|
||||
No hay un chat seleccionado
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Selecciona un chat del historial o crea uno nuevo para
|
||||
empezar.
|
||||
</p>
|
||||
@@ -808,7 +764,6 @@ function RouteComponent() {
|
||||
{chatMessages.map((msg: any) => {
|
||||
const isAI = msg.role === 'assistant'
|
||||
const isUser = msg.role === 'user'
|
||||
// IMPORTANTE: Asegúrate de que msg.id contenga la info de procesamiento o pásala en el map
|
||||
const isProcessing = msg.isProcessing
|
||||
|
||||
return (
|
||||
@@ -821,48 +776,44 @@ function RouteComponent() {
|
||||
<div
|
||||
className={`relative rounded-2xl p-3 text-sm whitespace-pre-wrap shadow-sm transition-all duration-300 ${
|
||||
isUser
|
||||
? 'rounded-tr-none bg-teal-600 text-white'
|
||||
: `rounded-tl-none border bg-white text-slate-700 ${
|
||||
? 'bg-primary text-primary-foreground rounded-tr-none'
|
||||
: `bg-card text-card-foreground rounded-tl-none border ${
|
||||
msg.isRefusal
|
||||
? 'border-red-200 bg-red-50/50 ring-1 ring-red-100'
|
||||
: 'border-slate-100'
|
||||
? 'border-destructive/50 bg-destructive/10 ring-destructive/20 ring-1'
|
||||
: 'border-border'
|
||||
}`
|
||||
}`}
|
||||
>
|
||||
{/* Aviso de Refusal */}
|
||||
{msg.isRefusal && (
|
||||
<div className="mb-1 flex items-center gap-1 text-[10px] font-bold text-red-500 uppercase">
|
||||
<div className="text-destructive mb-1 flex items-center gap-1 text-[10px] font-bold uppercase">
|
||||
<span>Aviso del Asistente</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CONTENIDO CORRECTO: Usamos msg.content */}
|
||||
{isAI && isProcessing ? (
|
||||
<div className="flex items-center gap-2 py-1">
|
||||
<div className="flex gap-1">
|
||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-teal-500" />
|
||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-teal-500 [animation-delay:-0.15s]" />
|
||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-teal-500 [animation-delay:-0.3s]" />
|
||||
<span className="bg-primary h-1.5 w-1.5 animate-bounce rounded-full" />
|
||||
<span className="bg-primary h-1.5 w-1.5 animate-bounce rounded-full [animation-delay:-0.15s]" />
|
||||
<span className="bg-primary h-1.5 w-1.5 animate-bounce rounded-full [animation-delay:-0.3s]" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
msg.content // <--- CAMBIO CLAVE
|
||||
msg.content
|
||||
)}
|
||||
|
||||
{/* Recomendaciones */}
|
||||
{isAI && msg.suggestions?.length > 0 && (
|
||||
<div className="mt-4 w-full space-y-3 rounded-xl border border-slate-100 bg-slate-50/50 p-3">
|
||||
<div className="border-border/60 bg-muted/50 mt-4 w-full space-y-3 rounded-xl border p-3">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase">
|
||||
<span className="text-muted-foreground text-[10px] font-bold uppercase">
|
||||
Sugerencias de mejora
|
||||
</span>
|
||||
{/* Botón Seleccionar Todo */}
|
||||
{msg.suggestions.some(
|
||||
(s: any) => !s.applied,
|
||||
) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-6 px-2 text-[10px] text-teal-600 hover:bg-teal-100"
|
||||
className="text-primary hover:bg-primary/10 h-6 px-2 text-[10px]"
|
||||
onClick={() =>
|
||||
toggleAllFromMessage(msg.suggestions)
|
||||
}
|
||||
@@ -883,7 +834,7 @@ function RouteComponent() {
|
||||
{!sug.applied && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-4 h-4 w-4 shrink-0 rounded border-slate-300 accent-teal-600"
|
||||
className="border-input accent-primary mt-4 h-4 w-4 shrink-0 rounded"
|
||||
checked={selectedImprovements.includes(
|
||||
sug.key,
|
||||
)}
|
||||
@@ -894,7 +845,7 @@ function RouteComponent() {
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<ImprovementCard
|
||||
suggestions={[sug]} // Pasamos solo una para que la card actúe individualmente
|
||||
suggestions={[sug]}
|
||||
dbMessageId={msg.dbMessageId}
|
||||
planId={planId}
|
||||
currentDatos={data?.datos}
|
||||
@@ -907,14 +858,13 @@ function RouteComponent() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Botón de Acción Masiva específico para este mensaje */}
|
||||
{msg.suggestions.some((s: any) =>
|
||||
selectedImprovements.includes(s.key),
|
||||
) && (
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={isSending}
|
||||
className="w-full bg-teal-600 py-1 text-xs font-bold text-white hover:bg-teal-700"
|
||||
className="w-full py-1 text-xs font-bold"
|
||||
onClick={() => {
|
||||
const seleccionadas =
|
||||
msg.suggestions.filter((s: any) =>
|
||||
@@ -952,20 +902,20 @@ function RouteComponent() {
|
||||
|
||||
{(isSending || isSyncing) && (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-2 flex gap-4">
|
||||
<Avatar className="h-9 w-9 shrink-0 border bg-teal-600 text-white shadow-sm">
|
||||
<Avatar className="bg-primary text-primary-foreground h-9 w-9 shrink-0 border shadow-sm">
|
||||
<AvatarFallback>
|
||||
<Sparkles size={16} className="animate-pulse" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<div className="rounded-2xl rounded-tl-none border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div className="bg-card border-border rounded-2xl rounded-tl-none border p-4 shadow-sm">
|
||||
<div className="flex gap-1">
|
||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400 [animation-delay:-0.3s]"></span>
|
||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400 [animation-delay:-0.15s]"></span>
|
||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400"></span>
|
||||
<span className="bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full [animation-delay:-0.3s]"></span>
|
||||
<span className="bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full [animation-delay:-0.15s]"></span>
|
||||
<span className="bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-medium text-slate-400 italic">
|
||||
<span className="text-muted-foreground text-[10px] font-medium italic">
|
||||
La IA está analizando tu solicitud...
|
||||
</span>
|
||||
</div>
|
||||
@@ -976,9 +926,8 @@ function RouteComponent() {
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Botones flotantes de aplicación */}
|
||||
{pendingSuggestion && !isLoading && (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-2 absolute bottom-4 left-1/2 z-10 flex -translate-x-1/2 gap-2 rounded-full border bg-white p-1.5 shadow-2xl">
|
||||
<div className="animate-in fade-in slide-in-from-bottom-2 bg-card border-border absolute bottom-4 left-1/2 z-10 flex -translate-x-1/2 gap-2 rounded-full border p-1.5 shadow-2xl">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -987,23 +936,19 @@ function RouteComponent() {
|
||||
>
|
||||
<X className="mr-1 h-3 w-3" /> Descartar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 rounded-full bg-teal-600 text-xs text-white hover:bg-teal-700"
|
||||
>
|
||||
<Button size="sm" className="h-8 rounded-full text-xs">
|
||||
<Check className="mr-1 h-3 w-3" /> Aplicar cambios
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
||||
<div className="shrink-0 border-t bg-white p-4">
|
||||
{/* INPUT FIJO AL FONDO */}
|
||||
<div className="bg-background border-border shrink-0 border-t p-4">
|
||||
<div className="relative mx-auto max-w-4xl">
|
||||
{/* MENÚ DE SUGERENCIAS FLOTANTE */}
|
||||
{showSuggestions && (
|
||||
<div className="animate-in slide-in-from-bottom-2 absolute bottom-full mb-2 w-full rounded-xl border bg-white shadow-2xl">
|
||||
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold text-slate-500 uppercase">
|
||||
<div className="animate-in slide-in-from-bottom-2 bg-popover border-border absolute bottom-full mb-2 w-full rounded-xl border shadow-2xl">
|
||||
<div className="bg-muted text-muted-foreground border-b px-3 py-2 text-[10px] font-bold uppercase">
|
||||
Resultados para "{filterQuery}"
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto p-1">
|
||||
@@ -1014,8 +959,8 @@ function RouteComponent() {
|
||||
onClick={() => toggleField(field)}
|
||||
className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition-colors ${
|
||||
index === 0
|
||||
? 'bg-teal-50 text-teal-700 ring-1 ring-teal-200 ring-inset'
|
||||
: 'hover:bg-slate-50'
|
||||
? 'bg-primary/10 text-primary ring-primary/30 ring-1 ring-inset'
|
||||
: 'hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
<span>{field.label}</span>
|
||||
@@ -1027,7 +972,7 @@ function RouteComponent() {
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="p-3 text-center text-xs text-slate-400">
|
||||
<div className="text-muted-foreground p-3 text-center text-xs">
|
||||
No hay coincidencias
|
||||
</div>
|
||||
)}
|
||||
@@ -1035,20 +980,18 @@ function RouteComponent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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 ) */}
|
||||
<div className="bg-muted/50 focus-within:bg-background focus-within:ring-primary flex flex-col gap-2 rounded-xl border p-2 transition-all focus-within:ring-1">
|
||||
{selectedFields.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 px-2 pt-1">
|
||||
{selectedFields.map((field) => (
|
||||
<div
|
||||
key={field.key}
|
||||
className="animate-in zoom-in-95 flex items-center gap-1 rounded-md border border-teal-200 bg-teal-100 px-2 py-0.5 text-[11px] font-semibold text-teal-800"
|
||||
className="animate-in zoom-in-95 border-primary/20 bg-primary/10 text-primary flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-semibold"
|
||||
>
|
||||
<span className="opacity-70">Campo:</span> {field.label}
|
||||
<button
|
||||
onClick={() => toggleField(field)}
|
||||
className="ml-1 rounded-full p-0.5 transition-colors hover:bg-teal-200"
|
||||
className="hover:bg-primary/20 ml-1 rounded-full p-0.5 transition-colors"
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
@@ -1057,7 +1000,6 @@ function RouteComponent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 2. Área de escritura */}
|
||||
<div className="flex items-end gap-2">
|
||||
<Textarea
|
||||
value={input}
|
||||
@@ -1076,7 +1018,6 @@ function RouteComponent() {
|
||||
setFilterQuery('')
|
||||
}
|
||||
} else {
|
||||
// Si el usuario borra y el input está vacío, eliminar el último campo
|
||||
if (
|
||||
e.key === 'Backspace' &&
|
||||
input === '' &&
|
||||
@@ -1104,7 +1045,7 @@ function RouteComponent() {
|
||||
isSending || (!input.trim() && selectedFields.length === 0)
|
||||
}
|
||||
size="icon"
|
||||
className="mb-1 h-9 w-9 shrink-0 bg-teal-600 hover:bg-teal-700"
|
||||
className="mb-1 h-9 w-9 shrink-0"
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
@@ -1118,22 +1059,22 @@ function RouteComponent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- PANEL LATERAL: ACCIONES RÁPIDAS (Escritorio) --- */}
|
||||
{/* --- PANEL LATERAL: ACCIONES RÁPIDAS --- */}
|
||||
<div className="hidden flex-[1] flex-col gap-4 overflow-y-auto 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 className="text-foreground flex items-center gap-2 text-left text-sm font-bold">
|
||||
<Lightbulb size={18} className="text-primary" /> Acciones rápidas
|
||||
</h4>
|
||||
<div className="space-y-2 p-1">
|
||||
{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 shadow-sm transition-all hover:border-teal-500 hover:bg-teal-50"
|
||||
className="bg-card hover:border-primary hover:bg-primary/5 group flex w-full items-center gap-3 rounded-xl border p-3 text-left text-sm shadow-sm transition-all"
|
||||
>
|
||||
<div className="rounded-lg bg-slate-100 p-2 text-slate-500 group-hover:bg-teal-100 group-hover:text-teal-600">
|
||||
<div className="bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary rounded-lg p-2 transition-colors">
|
||||
<preset.icon size={16} />
|
||||
</div>
|
||||
<span className="leading-tight font-medium text-slate-700">
|
||||
<span className="text-foreground leading-tight font-medium">
|
||||
{preset.label}
|
||||
</span>
|
||||
</button>
|
||||
@@ -1149,13 +1090,12 @@ function RouteComponent() {
|
||||
createNewChat()
|
||||
setIsHistoryOpen(false)
|
||||
}}
|
||||
className="mb-4 w-full bg-teal-600 text-white"
|
||||
className="mb-4 w-full"
|
||||
>
|
||||
<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">
|
||||
<p className="text-muted-foreground mb-4 text-xs font-bold uppercase">
|
||||
Historial Reciente
|
||||
</p>
|
||||
{activeChats.map((chat) => (
|
||||
@@ -1165,7 +1105,7 @@ function RouteComponent() {
|
||||
setActiveChatId(chat.id)
|
||||
setIsHistoryOpen(false)
|
||||
}}
|
||||
className="border-b p-3 text-sm"
|
||||
className="border-border border-b p-3 text-sm"
|
||||
>
|
||||
{chat.nombre || 'Chat sin nombre'}
|
||||
</div>
|
||||
@@ -1178,7 +1118,7 @@ function RouteComponent() {
|
||||
<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
|
||||
<Lightbulb size={18} className="text-primary" /> Acciones rápidas
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{PRESETS.map((preset) => (
|
||||
@@ -1188,7 +1128,7 @@ function RouteComponent() {
|
||||
handleSend(preset.prompt)
|
||||
setIsActionsOpen(false)
|
||||
}}
|
||||
className="flex items-center gap-3 rounded-xl border p-4 text-left text-sm"
|
||||
className="border-border flex items-center gap-3 rounded-xl border p-4 text-left text-sm"
|
||||
>
|
||||
<preset.icon size={16} />
|
||||
<span>{preset.label}</span>
|
||||
@@ -1198,23 +1138,20 @@ function RouteComponent() {
|
||||
</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 */}
|
||||
<div className="flex items-center justify-between border-b bg-slate-50/50 px-4 py-3">
|
||||
<h2 className="text-xs font-bold tracking-wider text-slate-500 uppercase">
|
||||
<DrawerContent className="bg-background 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 shadow-2xl">
|
||||
<div className="bg-muted/50 border-border flex items-center justify-between border-b px-4 py-3">
|
||||
<h2 className="text-muted-foreground text-xs font-bold tracking-wider uppercase">
|
||||
Referencias para la IA
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setOpenIA(false)}
|
||||
className="text-slate-400 transition-colors hover:text-slate-600"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Contenido con scroll interno */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<ReferenciasParaIA
|
||||
selectedArchivoIds={selectedArchivoIds}
|
||||
|
||||
Reference in New Issue
Block a user