Resumen de razonamiento
Línea plegable que resume lo que hizo el asistente y se abre en una línea de tiempo de pasos.
- Entender la preguntaPedidos con retraso en la semana en curso.
- Consultar pedidosFiltrados por fecha de entrega vencida.
- Cruzar con existenciasDos de los tres esperan material de bodega.
- Redactar la respuestaCada dato queda ligado a su fuente.
- Entender la preguntaPedidos con retraso en la semana en curso.
- Consultar pedidosFiltrados por fecha de entrega vencida.
- Cruzar con existenciasDos de los tres esperan material de bodega.
- Redactar la respuestaCada dato queda ligado a su fuente.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
/**
* Resumen plegable de lo que hizo un asistente antes de responder. Cerrado muestra una
* línea ("Razonó durante 4 s"); abierto, la lista de pasos como una línea de tiempo. Con
* `running` el último paso queda activo y el indicador gira. El panel se anima con
* grid-template-rows, sin alturas fijas. Los colores se sobrescriben con --gcai-ink,
* --gcai-surface, --gcai-line, --gcai-muted y --gcai-accent.
*
* <AiThinking
* summary="Razonó durante 4 s"
* steps={[{ title: "Consultar pedidos", detail: "Filtrados por fecha vencida." }]}
* />
*/
"use client";
import { useId, useState, type CSSProperties } from "react";
export type AiThinkingStep = { title: string; detail?: string };
export type AiThinkingProps = {
summary?: string;
steps?: AiThinkingStep[];
/** Mientras es true el último paso se marca como en curso. */
running?: boolean;
defaultOpen?: boolean;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gckt-root { --gckt-ink: var(--gcai-ink, #ededee); --gckt-muted: var(--gcai-muted, #9b9ca3); --gckt-line: var(--gcai-line, #26272c); --gckt-surface: var(--gcai-surface, #111113); --gckt-accent: var(--gcai-accent, #84b3ff); box-sizing: border-box; width: min(100%, 560px); margin-inline: auto; border: 1px solid var(--gckt-line); border-radius: 14px; background: var(--gckt-surface); color: var(--gckt-ink); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.gckt-toggle { display: flex; align-items: center; gap: 12px; width: 100%; padding: 13px 16px; border: 0; border-radius: 14px; background: transparent; color: inherit; font: inherit; font-weight: 500; text-align: left; cursor: pointer; }
.gckt-toggle:focus-visible { outline: 2px solid var(--gckt-accent); outline-offset: -2px; }
.gckt-mark { flex: none; width: 16px; height: 16px; fill: none; stroke: var(--gckt-muted); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.gckt-root[data-running="true"] .gckt-mark { stroke: var(--gckt-accent); animation: gckt-spin .9s linear infinite; }
.gckt-chevron { margin-left: auto; width: 14px; height: 14px; flex: none; fill: none; stroke: var(--gckt-muted); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; transition: transform .3s cubic-bezier(.16, 1, .3, 1); }
.gckt-toggle[aria-expanded="true"] .gckt-chevron { transform: rotate(180deg); }
.gckt-panel { display: grid; grid-template-rows: 0fr; visibility: hidden; transition: grid-template-rows .4s cubic-bezier(.16, 1, .3, 1), visibility 0s .4s; }
.gckt-panel[data-open="true"] { grid-template-rows: 1fr; visibility: visible; transition: grid-template-rows .4s cubic-bezier(.16, 1, .3, 1), visibility 0s; }
.gckt-clip { min-height: 0; overflow: hidden; }
.gckt-steps { position: relative; margin: 0; padding: 4px 16px 18px 16px; list-style: none; }
.gckt-step { position: relative; display: grid; gap: 1px; padding: 0 0 14px 26px; }
.gckt-step:last-child { padding-bottom: 0; }
.gckt-step::before { content: ""; position: absolute; left: 3px; top: 5px; width: 9px; height: 9px; box-sizing: border-box; border-radius: 50%; background: var(--gckt-ink); }
.gckt-step::after { content: ""; position: absolute; left: 7px; top: 17px; bottom: 2px; width: 1px; background: var(--gckt-line); }
.gckt-step:last-child::after { display: none; }
.gckt-step[data-active="true"]::before { background: var(--gckt-surface); border: 1.5px solid var(--gckt-accent); animation: gckt-pulse 1.3s ease-in-out infinite; }
.gckt-step b { font-weight: 600; }
.gckt-step span { color: var(--gckt-muted); font-size: 13px; }
@keyframes gckt-spin { to { transform: rotate(360deg); } }
@keyframes gckt-pulse { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .gckt-root[data-running="true"] .gckt-mark, .gckt-step[data-active="true"]::before { animation: none; } .gckt-panel, .gckt-panel[data-open="true"], .gckt-chevron { transition: none; } }
`;
const DEFAULT_STEPS: AiThinkingStep[] = [
{ title: "Entender la pregunta", detail: "Pedidos con retraso en la semana en curso." },
{ title: "Consultar pedidos", detail: "Filtrados por fecha de entrega vencida." },
{ title: "Cruzar con existencias", detail: "Dos de los tres esperan material de bodega." },
{ title: "Redactar la respuesta", detail: "Cada dato queda ligado a su fuente." },
];
export function AiThinking({
summary,
steps = DEFAULT_STEPS,
running = false,
defaultOpen = false,
className,
style,
}: AiThinkingProps) {
const panelId = useId();
const [open, setOpen] = useState(defaultOpen);
const label = summary ?? (running ? "Pensando…" : "Razonó durante 4 s");
return (
<>
<style>{CSS}</style>
<div className={["gckt-root", className].filter(Boolean).join(" ")} style={style} data-running={running}>
<button type="button" className="gckt-toggle" aria-expanded={open} aria-controls={panelId} onClick={() => setOpen((v) => !v)}>
{running ? (
<svg className="gckt-mark" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.75a6.25 6.25 0 1 0 6.25 6.25" /></svg>
) : (
<svg className="gckt-mark" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.25" /><path d="M5.2 8.2l2 2 3.6-4" /></svg>
)}
{label}
<svg className="gckt-chevron" viewBox="0 0 14 14" aria-hidden="true"><path d="M3 5l4 4 4-4" /></svg>
</button>
<div className="gckt-panel" id={panelId} data-open={open}>
<div className="gckt-clip">
<ol className="gckt-steps">
{steps.map((step, i) => (
<li key={step.title} className="gckt-step" data-active={running && i === steps.length - 1}>
<b>{step.title}</b>
{step.detail && <span>{step.detail}</span>}
</li>
))}
</ol>
</div>
</div>
</div>
</>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/ai-thinkingEjemplo de uso de Resumen de razonamiento
Después de copiar ai-thinking.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { AiThinking } from "./ai-thinking";
export default function Ejemplo() {
return <AiThinking summary="Ejemplo" running />;
}Propiedades de AiThinking
AiThinking acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
summary? | string | Opcional. |
steps? | AiThinkingStep[] | Opcional. |
running? | boolean | Mientras es true el último paso se marca como en curso. |
defaultOpen? | boolean | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Resumen de razonamiento 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-ink | #ededee |
--gcai-muted | #9b9ca3 |
--gcai-line | #26272c |
--gcai-surface | #111113 |
--gcai-accent | #84b3ff |
Accesibilidad y rendimiento
El archivo tiene 103 líneas (6 KB) y define 2 animaciones con @keyframes. Esto es lo que hace Resumen de razonamiento 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".
Acerca del producto
Cerrado ocupa una línea; abierto muestra los pasos con su detalle. Con running el último paso queda activo. El panel se anima sin alturas fijas y se oculta de teclado y lectores cuando está cerrado.
Incluye
Un solo archivo React + TypeScript con estilos dentro. Sin Tailwind ni dependencias externas.
- Pasos y detalle configurables
- Estado en curso con indicador
- Panel animado con grid-template-rows
- Oculto del teclado cuando está cerrado
- Respeta prefers-reduced-motion