Ir al contenido
Evaluar mi proyecto
Componentes · IADisponible

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.

ai-historial.tsx84 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/ai-historial

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
items?AiHistorialItem[]Opcional.
activeId?stringOpcional.
onSelect?(id: string) => voidOpcional.
onNew?() => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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.

VariableValor 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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)