Barra lateral de conversaciones
Búsqueda, grupos por fecha, conversación activa y botón de nueva conversación en una barra compacta.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useMemo, useState, type CSSProperties } from "react";
/** Barra lateral de conversaciones de un asistente: búsqueda, grupos por fecha, conversación activa y botón de nueva. Teclado con flechas. React + TypeScript, sin dependencias. Personaliza con --gcai-*. */
export type AiHistorialItem = { id: string; title: string; group: string };
export type AiHistorialProps = {
items?: AiHistorialItem[];
activeId?: string;
onSelect?: (id: string) => void;
onNew?: () => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcaihs-root { box-sizing: border-box; display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: 12px; width: 100%; max-width: 300px; height: 440px; padding: 12px; border: 1px solid var(--gcai-line, #26272c); border-radius: 14px; background: var(--gcai-surface, #111113); color: var(--gcai-ink, #ededee); font: 400 14px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcaihs-root *, .gcaihs-root *::before, .gcaihs-root *::after { box-sizing: border-box; }
.gcaihs-root button, .gcaihs-root input { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcaihs-root :focus-visible { outline: 2px solid var(--gcai-accent, #84b3ff); outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) { .gcaihs-root * { transition-duration: .01ms !important; } }
.gcaihs-new { display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 12px; border: 1px solid var(--gcai-line, #26272c); border-radius: 10px; background: var(--gcai-well, #1a1b1e); color: inherit; cursor: pointer; font-weight: 500; transition: border-color .15s; }
.gcaihs-new:hover { border-color: #3a3b42; }
.gcaihs-search { width: 100%; height: 38px; padding: 0 12px; border: 1px solid var(--gcai-line, #26272c); border-radius: 10px; background: transparent; color: inherit; }
.gcaihs-search::placeholder { color: #6a6b73; }
.gcaihs-scroll { overflow-y: auto; margin: 0 -4px; padding: 0 4px; scrollbar-width: thin; scrollbar-color: #2c2d33 transparent; }
.gcaihs-group { margin: 14px 0 6px 8px; font-size: 12px; font-weight: 500; color: var(--gcai-muted, #9b9ca3); }
.gcaihs-group:first-child { margin-top: 0; }
.gcaihs-list { margin: 0; padding: 0; list-style: none; }
.gcaihs-item { display: block; width: 100%; padding: 8px 10px; overflow: hidden; border: 0; border-radius: 8px; background: transparent; color: inherit; text-align: left; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; transition: background .12s; }
.gcaihs-item:hover { background: #17171a; }
.gcaihs-item[aria-current="true"] { background: var(--gcai-well, #1a1b1e); color: #fff; }
.gcaihs-empty { padding: 20px 8px; color: var(--gcai-muted, #9b9ca3); font-size: 13px; }`;
const DEFAULT: AiHistorialItem[] = [
{ id: "1", title: "Resumen de la reunión semanal", group: "Hoy" },
{ id: "2", title: "Correo para proveedor de insumos", group: "Hoy" },
{ id: "3", title: "Cómo automatizar los reportes de ventas", group: "Ayer" },
{ id: "4", title: "Plan de migración de base de datos", group: "Ayer" },
{ id: "5", title: "Ideas para el nombre del proyecto", group: "Últimos 7 días" },
{ id: "6", title: "Revisión de contrato de servicio", group: "Últimos 7 días" },
{ id: "7", title: "Checklist de seguridad para accesos", group: "Últimos 7 días" },
];
export function AiHistorial({ items = DEFAULT, activeId, onSelect, onNew, className = "", style }: AiHistorialProps) {
const [query, setQuery] = useState("");
const [active, setActive] = useState(activeId ?? items[0]?.id);
const filtered = useMemo(() => items.filter((item) => item.title.toLowerCase().includes(query.trim().toLowerCase())), [items, query]);
const groups = useMemo(() => {
const map = new Map<string, AiHistorialItem[]>();
filtered.forEach((item) => map.set(item.group, [...(map.get(item.group) ?? []), item]));
return [...map.entries()];
}, [filtered]);
return (
<aside className={`gcaihs-root ${className}`} style={style} aria-label="Conversaciones">
<style>{CSS}</style>
<button className="gcaihs-new" type="button" onClick={onNew}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
Nueva conversación
</button>
<input className="gcaihs-search" type="search" aria-label="Buscar conversaciones" placeholder="Buscar" value={query} onChange={(event) => setQuery(event.target.value)} />
<div className="gcaihs-scroll">
{groups.length === 0 ? <p className="gcaihs-empty">Ninguna conversación coincide con “{query}”.</p> : null}
{groups.map(([group, list]) => (
<section key={group}>
<h3 className="gcaihs-group">{group}</h3>
<ul className="gcaihs-list">
{list.map((item) => (
<li key={item.id}>
<button className="gcaihs-item" type="button" aria-current={active === item.id} title={item.title} onClick={() => { setActive(item.id); onSelect?.(item.id); }}>
{item.title}
</button>
</li>
))}
</ul>
</section>
))}
</div>
</aside>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/ai-historialEjemplo de uso de Barra lateral de conversaciones
Después de copiar ai-historial.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { AiHistorial } from "./ai-historial";
export default function Ejemplo() {
return <AiHistorial activeId="Ejemplo" />;
}Propiedades de AiHistorial
AiHistorial acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
items? | AiHistorialItem[] | Opcional. |
activeId? | string | Opcional. |
onSelect? | (id: string) => void | Opcional. |
onNew? | () => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Barra lateral de conversaciones con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--gcai-line | #26272c |
--gcai-surface | #111113 |
--gcai-ink | #ededee |
--gcai-accent | #84b3ff |
--gcai-well | #1a1b1e |
--gcai-muted | #9b9ca3 |
Accesibilidad y rendimiento
El archivo tiene 84 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Barra lateral de conversaciones por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".