Ir al contenido
Evaluar mi proyecto
Componentes · IADisponible

Entrada por voz con onda

Botón de micrófono, onda de nivel y transcripción que aparece palabra a palabra. Simulación visual, sin acceso al micrófono.

Pulsa el micrófono para dictar

Tu mensaje aparecerá aquí.

Código completo

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

ai-voz.tsx88 líneas
Descargar
"use client";

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

/** Entrada por voz de un asistente: botón de micrófono, onda de nivel y transcripción que va apareciendo. Simulación visual, no accede al micrófono. React + TypeScript, sin dependencias. Personaliza con --gcai-*. */
export type AiVozProps = {
  phrase?: string;
  onDone?: (text: string) => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcaivz-root { box-sizing: border-box; width: 100%; max-width: 460px; padding: 20px; border: 1px solid var(--gcai-line, #26272c); border-radius: 16px; background: var(--gcai-surface, #111113); color: var(--gcai-ink, #ededee); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcaivz-root *, .gcaivz-root *::before, .gcaivz-root *::after { box-sizing: border-box; }
.gcaivz-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcaivz-root :focus-visible { outline: 2px solid var(--gcai-accent, #84b3ff); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gcaivz-root * { animation: none !important; transition-duration: .01ms !important; } }
.gcaivz-row { display: flex; align-items: center; gap: 16px; }
.gcaivz-mic { flex: none; display: grid; place-items: center; width: 52px; height: 52px; border: 1px solid var(--gcai-line, #26272c); border-radius: 50%; background: var(--gcai-well, #1a1b1e); color: var(--gcai-ink, #ededee); cursor: pointer; transition: background .15s, border-color .15s, color .15s; }
.gcaivz-mic:hover { border-color: #3a3b42; }
.gcaivz-mic[aria-pressed="true"] { background: var(--gcai-accent, #84b3ff); border-color: var(--gcai-accent, #84b3ff); color: #0a0a0b; }
.gcaivz-wave { flex: 1; display: flex; align-items: center; gap: 3px; height: 44px; }
.gcaivz-wave i { flex: 1; height: 4px; max-width: 5px; border-radius: 3px; background: #3a3b42; transform-origin: 50% 50%; transition: background .2s; }
.gcaivz-root[data-on="true"] .gcaivz-wave i { background: var(--gcai-accent, #84b3ff); animation: gcaivz-bar var(--t) ease-in-out var(--d) infinite alternate; }
.gcaivz-state { margin: 16px 0 0; font-size: 12px; color: var(--gcai-muted, #9b9ca3); letter-spacing: .02em; }
.gcaivz-text { min-height: 66px; margin: 8px 0 0; font-size: 17px; line-height: 1.45; }
.gcaivz-text em { color: var(--gcai-muted, #9b9ca3); font-style: normal; }
@keyframes gcaivz-bar { from { transform: scaleY(1); } to { transform: scaleY(var(--h)); } }`;

const WORDS_DELAY = 260;

export function AiVoz({ phrase = "Programa una reunión con el equipo de operaciones para el jueves a las diez", onDone, className = "", style }: AiVozProps) {
  const [on, setOn] = useState(false);
  const [shown, setShown] = useState(0);
  const timer = useRef(0);
  const words = phrase.split(" ");

  useEffect(() => {
    if (!on) return;
    timer.current = window.setInterval(() => {
      setShown((value) => {
        if (value + 1 >= words.length) {
          window.clearInterval(timer.current);
          window.setTimeout(() => setOn(false), 600);
          return words.length;
        }
        return value + 1;
      });
    }, WORDS_DELAY);
    return () => window.clearInterval(timer.current);
  }, [on, words.length]);

  function toggle() {
    if (on) {
      setOn(false);
      onDone?.(words.slice(0, shown).join(" "));
    } else {
      setShown(0);
      setOn(true);
    }
  }

  return (
    <div className={`gcaivz-root ${className}`} style={style} data-on={on}>
      <style>{CSS}</style>
      <div className="gcaivz-row">
        <button className="gcaivz-mic" type="button" aria-pressed={on} aria-label={on ? "Detener dictado" : "Empezar a dictar"} onClick={toggle}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            {on ? <rect x="7" y="7" width="10" height="10" rx="2" fill="currentColor" /> : <><rect x="9" y="3" width="6" height="11" rx="3" /><path d="M5 11a7 7 0 0 0 14 0M12 18v3" /></>}
          </svg>
        </button>
        <div className="gcaivz-wave" aria-hidden="true">
          {Array.from({ length: 36 }, (_, i) => (
            <i key={i} style={{ "--h": 2 + ((i * 7) % 9), "--t": `${0.45 + ((i * 13) % 5) * 0.12}s`, "--d": `${-((i * 3) % 7) * 0.1}s` } as CSSProperties} />
          ))}
        </div>
      </div>
      <p className="gcaivz-state" role="status">{on ? "Escuchando… (simulación)" : shown ? "Dictado completo" : "Pulsa el micrófono para dictar"}</p>
      <p className="gcaivz-text">
        {words.slice(0, shown).join(" ")}
        {on ? <em> …</em> : null}
        {!shown && !on ? <em>Tu mensaje aparecerá aquí.</em> : null}
      </p>
    </div>
  );
}

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

Ejemplo de uso de Entrada por voz con onda

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

ejemplo.tsx5 líneas
import { AiVoz } from "./ai-voz";

export default function Ejemplo() {
  return <AiVoz phrase="Ejemplo" />;
}

Propiedades de AiVoz

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

PropiedadTipoDescripción
phrase?stringOpcional.
onDone?(text: string) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Entrada por voz con onda 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 88 líneas (5 KB) y define 1 animación con @keyframes. Esto es lo que hace Entrada por voz con onda 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)