ThunderPhone 2.0 ya está disponible.Empieza por tu cuenta desde 2¢/min.Lee el anuncio

Widget

Hook sin interfaz

Crea una interfaz de voz totalmente personalizada con el hook de React useThunderPhone

El hook useThunderPhone te brinda control total sobre la interfaz de usuario mientras ThunderPhone administra la sesión de voz, el enrutamiento de audio y el estado de conexión. Úsalo cuando quieras una interfaz totalmente personalizada -- tus propios botones, diseños, animaciones e identidad de marca -- mientras ThunderPhone se encarga de todo internamente.

Cuándo usar el hook sin interfaz

El componente predefinido ThunderPhoneWidget cubre la mayoría de los casos de uso, pero usa el hook sin interfaz cuando necesites:

  • Una interfaz de llamadas completamente personalizada que coincida con el sistema de diseño de tu app
  • Visualizaciones que reaccionen al audio (formas de onda, esferas, indicadores pulsantes) impulsadas por niveles de audio en tiempo real
  • Flujos de llamadas personalizados, como formularios previos a la llamada, encuestas posteriores a la llamada o chat integrado junto con la voz
  • Integración en una biblioteca de componentes existente (Material UI, Chakra, Radix, etc.)

Instalación

npm install @thunderphone/widget

Uso básico

import { useThunderPhone } from '@thunderphone/widget'
 
function CustomCallButton() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
 
  const handleClick = () => {
    if (phone.state === 'connected') {
      phone.disconnect()
    } else {
      phone.connect()
    }
  }
 
  return (
    <>
      <button onClick={handleClick} disabled={phone.state === 'connecting'}>
        {phone.state === 'connecting'
          ? 'Connecting...'
          : phone.state === 'connected'
            ? 'End call'
            : 'Start call'}
      </button>
      {phone.audio}
    </>
  )
}

Opciones

Pasa estas opciones a useThunderPhone mediante UseThunderPhoneOptions:

OpciónTipoObligatorioPredeterminadoDescripción
publishableKeystring--Clave de API publicable (pk_live_...). El agente se resuelve automáticamente a partir de la configuración del widget de la clave.
apiBasestringNo'https://api.thunderphone.com/v1'Anulación de la URL base de la API.
languagestringNo--Anulación de idioma por sesión -- un código de idioma o configuración regional como en, es o fr-FR. Cuando no se configura, se aplica el idioma configurado del agente.
voicestringNo--Anulación de voz por sesión -- un nombre de voz como maria. Cuando no se configura, se aplica la voz configurada del agente.
contextstringNo--Contexto factual de la página o el sitio por sesión que se pasa al agente. Se trunca del lado del servidor a 12,000 caracteres.
onConnect() => voidNo--Se llama cuando se conecta la sesión de voz.
onDisconnect() => voidNo--Se llama cuando finaliza la sesión.
onError(error) => voidNo--Se llama cuando ocurren errores. El error tiene los campos error (código) y message.
ringtoneboolean | stringNofalseReproduce un tono de llamada mientras se conecta. true para el tono de llamada predeterminado o una cadena de URL para audio personalizado.

Valor de retorno

El hook devuelve un objeto UseThunderPhoneReturn:

PropiedadTipoDescripción
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Estado actual de la conexión.
connect() => voidInicia una sesión de voz.
disconnect() => voidFinaliza la sesión actual.
toggleMute() => voidActiva o desactiva el silencio del micrófono.
isMutedbooleanIndica si el micrófono está actualmente silenciado.
errorstring | undefinedMensaje de error cuando el estado es 'error'.
agentNamestring | undefinedNombre para mostrar del agente conectado.
audioLevelnumberObsoleto -- siempre es 0. Un marcador de posición estático que se conserva para mantener la compatibilidad con versiones anteriores; nunca se actualiza. Lee audioLevelRef.current en su lugar.
audioLevelRefReact.RefObject<number>Una ref mutable que contiene el nivel de audio en tiempo real (0--1) -- el más alto entre la voz del agente y el micrófono del visitante -- actualizada en cada cuadro de animación, fuera del ciclo de renderizado de React. Lee audioLevelRef.current dentro de bucles de requestAnimationFrame para obtener animaciones fluidas y sin interrupciones, o consúltala a intervalos cuando necesites el valor en el estado de React.
audioReactNodeElemento invisible que gestiona la conexión de audio -- debe renderizarse.

IU reactiva al audio

La referencia audioLevelRef te proporciona niveles de audio a velocidad de fotogramas sin activar nuevas renderizaciones de React, por lo que es ideal para controlar visualizaciones fluidas de formas de onda, orbes pulsantes o cualquier animación vinculada a la conversación. El nivel refleja cuál es más fuerte: la voz del agente o el micrófono del visitante.

Ejemplo de forma de onda

import { useRef, useEffect } from 'react'
import { useThunderPhone } from '@thunderphone/widget'
 
function WaveformCall() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
  const canvasRef = useRef<HTMLCanvasElement>(null)
 
  useEffect(() => {
    if (phone.state !== 'connected') return
    const canvas = canvasRef.current
    if (!canvas) return
    const ctx = canvas.getContext('2d')!
 
    let animId: number
    const draw = () => {
      const level = phone.audioLevelRef.current ?? 0
      ctx.clearRect(0, 0, canvas.width, canvas.height)
 
      // Draw bars that react to audio level
      const barCount = 24
      const barWidth = canvas.width / barCount
      for (let i = 0; i < barCount; i++) {
        const distance = Math.abs(i - barCount / 2) / (barCount / 2)
        const height = level * canvas.height * (1 - distance * 0.6)
        const y = (canvas.height - height) / 2
        ctx.fillStyle = '#0ea5e9'
        ctx.fillRect(i * barWidth + 1, y, barWidth - 2, height)
      }
 
      animId = requestAnimationFrame(draw)
    }
    animId = requestAnimationFrame(draw)
    return () => cancelAnimationFrame(animId)
  }, [phone.state, phone.audioLevelRef])
 
  return (
    <div>
      {phone.state === 'connected' && (
        <canvas ref={canvasRef} width={240} height={80} />
      )}
      <button
        onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}
        disabled={phone.state === 'connecting'}
      >
        {phone.state === 'connected' ? 'End call' : 'Start call'}
      </button>
      {phone.audio}
    </div>
  )
}

Ejemplo de orbe pulsante

import { useRef, useEffect } from 'react'
import { useThunderPhone } from '@thunderphone/widget'
 
function PulsingOrb() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
  const orbRef = useRef<HTMLDivElement>(null)
 
  useEffect(() => {
    if (phone.state !== 'connected') return
    let animId: number
    const animate = () => {
      const level = phone.audioLevelRef.current ?? 0
      if (orbRef.current) {
        const scale = 1 + level * 0.5
        orbRef.current.style.transform = `scale(${scale})`
        orbRef.current.style.opacity = `${0.6 + level * 0.4}`
      }
      animId = requestAnimationFrame(animate)
    }
    animId = requestAnimationFrame(animate)
    return () => cancelAnimationFrame(animId)
  }, [phone.state, phone.audioLevelRef])
 
  return (
    <div style={{ textAlign: 'center' }}>
      <div
        ref={orbRef}
        style={{
          width: 80,
          height: 80,
          borderRadius: '50%',
          background: '#0ea5e9',
          margin: '20px auto',
          transition: 'transform 0.05s ease-out',
        }}
      />
      <button
        onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}
        disabled={phone.state === 'connecting'}
      >
        {phone.state === 'connected' ? 'End call' : 'Call'}
      </button>
      {phone.audio}
    </div>
  )
}

Ejemplo de indicador de habla

Para una IU renderizada por React que cambie con el volumen —como una insignia de "hablando" basada en un umbral—, consulta audioLevelRef.current en un intervalo y guarda el resultado en el estado:

import { useEffect, useState } from 'react'
import { useThunderPhone } from '@thunderphone/widget'
 
function SpeakingBadge() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
  const [speaking, setSpeaking] = useState(false)
 
  useEffect(() => {
    if (phone.state !== 'connected') {
      setSpeaking(false)
      return
    }
    const interval = setInterval(() => {
      setSpeaking((phone.audioLevelRef.current ?? 0) > 0.1)
    }, 100)
    return () => clearInterval(interval)
  }, [phone.state, phone.audioLevelRef])
 
  return (
    <div>
      {phone.state === 'connected' && (
        <span>{speaking ? 'Speaking' : 'Listening'}</span>
      )}
      <button onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}>
        {phone.state === 'connected' ? 'End call' : 'Start call'}
      </button>
      {phone.audio}
    </div>
  )
}

Máquina de estados

La propiedad state sigue este ciclo de vida:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
EstadoDescripción
idleNo hay ninguna sesión activa. Listo para llamar a connect().
connectingSe está estableciendo la sesión. Desactiva el botón de llamada durante este estado.
connectedLa sesión de voz está activa. El usuario está hablando con el agente.
disconnectedLa sesión finalizó correctamente. Vuelve automáticamente a idle después de 1.5 segundos.
errorAlgo salió mal. Consulta phone.error para ver el mensaje. El estado no se borra por sí solo -- llamar a connect() de nuevo inicia un intento nuevo y restablece el error.

Ejemplos

Con control de silencio

import { useThunderPhone } from '@thunderphone/widget'
 
function CallWithMute() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
 
  return (
    <div>
      {phone.state === 'connected' && (
        <div>
          <p>Talking to {phone.agentName ?? 'Agent'}</p>
          <button onClick={phone.toggleMute}>
            {phone.isMuted ? 'Unmute' : 'Mute'}
          </button>
          <button onClick={phone.disconnect}>End call</button>
        </div>
      )}
 
      {phone.state !== 'connected' && (
        <button
          onClick={phone.connect}
          disabled={phone.state === 'connecting'}
        >
          {phone.state === 'connecting' ? 'Connecting...' : 'Call support'}
        </button>
      )}
 
      {phone.state === 'error' && (
        <p style={{ color: 'red' }}>{phone.error}</p>
      )}
 
      {phone.audio}
    </div>
  )
}

Con tono de llamada

Reproduce un tono mientras se conecta para simular una llamada telefónica:

import { useThunderPhone } from '@thunderphone/widget'
 
function PhoneCallButton() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
    ringtone: true, // or a custom URL: 'https://example.com/ringtone.mp3'
  })
 
  return (
    <>
      <button
        onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}
        disabled={phone.state === 'connecting'}
      >
        {phone.state === 'connecting'
          ? 'Ringing...'
          : phone.state === 'connected'
            ? 'Hang up'
            : 'Call'}
      </button>
      {phone.audio}
    </>
  )
}

El tono de llamada se repite durante el estado connecting y se desvanece cuando el agente se conecta. Pasa true para usar el tono de llamada predeterminado integrado o una cadena de URL para usar tu propio archivo de audio.

Con callbacks de eventos

import { useThunderPhone } from '@thunderphone/widget'
 
function TrackedCallButton() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
    onConnect: () => {
      analytics.track('call_started')
    },
    onDisconnect: () => {
      analytics.track('call_ended')
    },
    onError: (error) => {
      analytics.track('call_error', { code: error.error, message: error.message })
    },
  })
 
  return (
    <>
      <button
        onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}
        disabled={phone.state === 'connecting'}
      >
        {phone.state === 'connected' ? 'Hang up' : 'Talk to AI'}
      </button>
      {phone.audio}
    </>
  )
}

Interfaz de usuario totalmente personalizada

import { useThunderPhone } from '@thunderphone/widget'
 
function FullCustomUI() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
 
  return (
    <div className="call-panel">
      <div className="call-status">
        {phone.state === 'idle' && <span>Ready</span>}
        {phone.state === 'connecting' && <span className="pulse">Connecting...</span>}
        {phone.state === 'connected' && (
          <span>On call with {phone.agentName}</span>
        )}
        {phone.state === 'error' && <span className="error">{phone.error}</span>}
      </div>
 
      <div className="call-controls">
        {phone.state === 'connected' ? (
          <>
            <button className="mute-btn" onClick={phone.toggleMute}>
              {phone.isMuted ? 'Unmute' : 'Mute'}
            </button>
            <button className="end-btn" onClick={phone.disconnect}>
              End
            </button>
          </>
        ) : (
          <button
            className="start-btn"
            onClick={phone.connect}
            disabled={phone.state === 'connecting'}
          >
            Start call
          </button>
        )}
      </div>
 
      {/* Required -- handles audio under the hood */}
      {phone.audio}
    </div>
  )
}

Consejos

Renderiza siempre phone.audio

El elemento phone.audio es invisible, pero obligatorio. Colócalo en cualquier lugar de tu JSX -- no renderiza ningún DOM visible, pero administra internamente la conexión de audio WebRTC.

Desactiva el botón mientras se conecta

El estado connecting puede durar entre 1 y 3 segundos. Desactiva el botón de llamada durante este estado para evitar intentos de conexión duplicados.

Maneja el estado de error correctamente

Cuando el estado sea error, muestra phone.error a la persona usuaria y mantén habilitado el botón de llamada. El hook no sale del estado error por sí solo -- volver a llamar a connect() inicia un intento nuevo y borra el error anterior.

Usa callbacks para efectos secundarios

Los callbacks onConnect, onDisconnect y onError son ideales para analítica, registros o para activar otra lógica de la aplicación sin consultar el estado constantemente.

Lee los niveles de audio desde audioLevelRef

audioLevelRef es la única fuente activa de niveles de audio. Lee audioLevelRef.current dentro de requestAnimationFrame para obtener animaciones fluidas, como formas de onda (leer una referencia no provoca nuevos renderizados), o toma muestras a intervalos y guarda el resultado en el estado para una interfaz renderizada por React. El número audioLevel está obsoleto y siempre es 0 -- no construyas lógica basándote en él.