Ir al contenido
Evaluar mi proyecto
Componentes · IADisponible

Aprobación de acciones del asistente

Revisión explícita del alcance, datos y destino de una acción de IA; aprobar o rechazar mediante callback asíncrono.

Revisar antes de continuar

El asistente propone preparar una exportación. Revisa el alcance y decide.

Acción
Preparar un archivo CSV
Contenido
Datos de muestra seleccionados
Destino
Descarga local

Solo continúa después de tu elección.

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

ai-aprobacion.tsx142 líneas
Descargar
"use client";

import { useState, useEffect, useRef, type CSSProperties } from "react";

/** Aprobación de acciones del asistente. React + TypeScript, sin dependencias. Personaliza con variables --gczh-*. */
export type AiAprobacionProps = {
  title?: string;
  description?: string;
  details?: { label: string; value: string }[];
  onDecision?: (approved: boolean) => Promise<void> | void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gczh-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gczh-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gczh-root *, .gczh-root *::before, .gczh-root *::after { box-sizing: border-box; }
.gczh-root button, .gczh-root input, .gczh-root textarea, .gczh-root select { font: inherit; }
.gczh-root button, .gczh-root a { -webkit-tap-highlight-color: transparent; }
.gczh-root :focus-visible { outline: 2px solid var(--gczh-accent, #25724b); outline-offset: 4px; }
.gczh-root ::selection { background: var(--gczh-accent, #25724b); color: #fff; }
.gczh-root button:disabled { opacity: .5; cursor: not-allowed; }
.gczh-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gczh-root *, .gczh-root *::before, .gczh-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gczh-root { width:480px; padding:24px; border:1px solid #c7d2ce; border-radius:12px; background:var(--gczh-bg,#fff); }
.gczh-heading { display:flex; align-items:center; gap:12px; color:#325342; }
.gczh-heading h3 { font-size:18px; font-weight:600; margin:0; }
.gczh-root>p { color:#5a695f; font-size:13px; margin:14px 0 20px; }
.gczh-root dl { margin:0 0 24px; }
.gczh-root dl>div { display:flex; justify-content:space-between; align-items:baseline; gap:24px; padding:11px 0; border-bottom:1px solid #e3e7e1; font-size:12px; }
.gczh-root dt { color:#5a695f; flex-shrink:0; }
.gczh-root dd { color:#293f30; text-align:right; margin:0; overflow-wrap:anywhere; }
.gczh-actions { display:flex; gap:10px; }
.gczh-actions button,.gczh-reset { border:1px solid #bccabd; border-radius:6px; min-height:44px; padding:10px 20px; cursor:pointer; background:#fff; color:#35563f; font-size:13px!important; }
.gczh-actions button:last-child { background:var(--gczh-accent,#345c42); color:#fff; border-color:transparent; }
.gczh-actions button:hover:not(:disabled) { filter:brightness(.95); }
.gczh-root .gczh-status { font-size:11px; margin:18px 0 0; }`;

export function AiAprobacion({
  title = "Revisar antes de continuar",
  description = "El asistente propone preparar una exportación. Revisa el alcance y decide.",
  details = [
    { label: "Acción", value: "Preparar un archivo CSV" },
    { label: "Contenido", value: "Datos de muestra seleccionados" },
    { label: "Destino", value: "Descarga local" },
  ],
  onDecision,
  className = "",
  style,
}: AiAprobacionProps) {
  const [state, setState] = useState<
    "pending" | "busy" | "approved" | "rejected" | "error"
  >("pending");
  const mounted = useRef(false);
  const busy = useRef(false);
  useEffect(() => {
    mounted.current = true;
    return () => {
      mounted.current = false;
    };
  }, []);
  async function decide(approved: boolean) {
    if (busy.current) return;
    busy.current = true;
    setState("busy");
    try {
      await onDecision?.(approved);
      if (mounted.current) setState(approved ? "approved" : "rejected");
    } catch {
      if (mounted.current) setState("error");
    } finally {
      busy.current = false;
    }
  }
  return (
    <section className={`gczh-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gczh-heading">
        <svg
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          aria-hidden="true"
        >
          <path d="m12 3 8 3v6c0 5-8 9-8 9s-8-4-8-9V6Z" />
          <path d="m8 12 3 3 5-6" />
        </svg>
        <h3>{title}</h3>
      </div>
      <p>{description}</p>
      <dl>
        {details.map((detail, i) => (
          <div key={i}>
            <dt>{detail.label}</dt>
            <dd>{detail.value}</dd>
          </div>
        ))}
      </dl>
      {state === "approved" || state === "rejected" ? (
        <button
          type="button"
          className="gczh-reset"
          onClick={() => setState("pending")}
        >
          Revisar de nuevo
        </button>
      ) : (
        <div className="gczh-actions">
          <button
            type="button"
            disabled={state === "busy"}
            onClick={() => decide(false)}
          >
            Rechazar
          </button>
          <button
            type="button"
            disabled={state === "busy"}
            onClick={() => decide(true)}
          >
            {state === "busy" ? "Procesando…" : "Aprobar acción"}
          </button>
        </div>
      )}
      <p className="gczh-status" role="status">
        {state === "approved"
          ? onDecision
            ? "Acción aprobada."
            : "Aprobación de muestra registrada. No se ejecutó ninguna acción."
          : state === "rejected"
            ? "Acción rechazada."
            : state === "error"
              ? "No se pudo registrar la decisión. Inténtalo de nuevo."
              : "Solo continúa después de tu elección."}
      </p>
    </section>
  );
}

Ú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-aprobacion

Ejemplo de uso de Aprobación de acciones del asistente

Después de copiar ai-aprobacion.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { AiAprobacion } from "./ai-aprobacion";

export default function Ejemplo() {
  return <AiAprobacion title="Ejemplo" />;
}

Propiedades de AiAprobacion

AiAprobacion acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
title?stringOpcional.
description?stringOpcional.
details?{ label: string; value: string }[]Opcional.
onDecision?(approved: boolean) => Promise<void> | voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Aprobación de acciones del asistente 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
--gczh-fg#202421
--gczh-accent#25724b
--gczh-bg#fff

Accesibilidad y rendimiento

El archivo tiene 142 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Aprobación de acciones del asistente 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).
  • Declara roles ARIA para lectores de pantalla: status.
  • 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)