ThunderPhone 2.0 já está no ar.Comece por conta própria, a partir de 2¢/min.Leia o anúncio

Widget

Hook Headless

Crie uma interface de voz totalmente personalizada com o hook React useThunderPhone

O hook useThunderPhone oferece controle total sobre a interface do usuário enquanto o ThunderPhone gerencia a sessão de voz, o roteamento de áudio e o estado da conexão. Use-o quando quiser uma UI totalmente personalizada -- com seus próprios botões, layouts, animações e identidade visual -- enquanto o ThunderPhone cuida de tudo nos bastidores.

Quando usar o hook headless

O componente pré-criado ThunderPhoneWidget cobre a maioria dos casos de uso, mas use o hook headless quando precisar de:

  • Uma UI de chamada totalmente personalizada que corresponda ao sistema de design do seu app
  • Visualizações reativas ao áudio (formas de onda, orbes, indicadores pulsantes) orientadas por níveis de áudio em tempo real
  • Fluxos de chamada personalizados, como formulários antes da chamada, pesquisas após a chamada ou chat integrado ao lado da voz
  • Integração com uma biblioteca de componentes existente (Material UI, Chakra, Radix etc.)

Instalação

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}
    </>
  )
}

Opções

Passe estas opções para useThunderPhone por meio de UseThunderPhoneOptions:

OpçãoTipoObrigatóriaPadrãoDescrição
publishableKeystringSim--Chave de API publicável (pk_live_...). O agente é determinado automaticamente pela configuração do widget da chave.
apiBasestringNão'https://api.thunderphone.com/v1'Substituição da URL base da API.
languagestringNão--Substituição de idioma por sessão -- um código de idioma ou localidade, como en, es ou fr-FR. Quando não definido, aplica-se o idioma configurado do agente.
voicestringNão--Substituição de voz por sessão -- um nome de voz, como maria. Quando não definido, aplica-se a voz configurada do agente.
contextstringNão--Contexto factual da página ou do site por sessão enviado ao agente. Truncado no servidor para 12.000 caracteres.
onConnect() => voidNão--Chamado quando a sessão de voz é conectada.
onDisconnect() => voidNão--Chamado quando a sessão termina.
onError(error) => voidNão--Chamado em caso de erros. O erro tem os campos error (código) e message.
ringtoneboolean | stringNãofalseReproduz um toque enquanto conecta. Use true para o toque padrão ou uma string de URL para áudio personalizado.

Valor de retorno

O hook retorna um objeto UseThunderPhoneReturn:

PropriedadeTipoDescrição
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Estado atual da conexão.
connect() => voidInicia uma sessão de voz.
disconnect() => voidEncerra a sessão atual.
toggleMute() => voidAtiva ou desativa o silenciamento do microfone.
isMutedbooleanIndica se o microfone está silenciado no momento.
errorstring | undefinedMensagem de erro quando o estado é 'error'.
agentNamestring | undefinedNome de exibição do agente conectado.
audioLevelnumberObsoleto -- sempre 0. Um placeholder estático mantido para compatibilidade retroativa; ele nunca é atualizado. Leia audioLevelRef.current em vez disso.
audioLevelRefReact.RefObject<number>Uma ref mutável que contém o nível de áudio em tempo real (0--1) -- o mais alto entre a voz do agente e o microfone do visitante -- atualizada em cada frame de animação, fora do ciclo de renderização do React. Leia audioLevelRef.current dentro de loops requestAnimationFrame para animações fluidas, sem travamentos, ou faça uma amostragem em um intervalo quando precisar do valor no estado do React.
audioReactNodeElemento invisível que gerencia a conexão de áudio -- deve ser renderizado.

IU reativa a áudio

A ref audioLevelRef fornece níveis de áudio em taxa de quadros sem acionar novas renderizações do React, o que a torna ideal para controlar visualizações suaves de forma de onda, orbes pulsantes ou qualquer animação vinculada à conversa. O nível reflete o que estiver mais alto: a voz do agente ou o microfone do visitante.

Exemplo 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>
  )
}

Exemplo 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>
  )
}

Exemplo de indicador de fala

Para uma IU renderizada pelo React que muda conforme o volume -- como um selo de "falando" baseado em limite -- faça amostragens de audioLevelRef.current em um intervalo e armazene o resultado no 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

A propriedade state segue este ciclo de vida:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
EstadoDescrição
idleNenhuma sessão ativa. Pronto para chamar connect().
connectingA sessão está sendo estabelecida. Desative o botão de chamada durante este estado.
connectedA sessão de voz está ativa. O usuário está falando com o agente.
disconnectedA sessão foi encerrada corretamente. Faz a transição de volta para idle automaticamente após 1,5 segundos.
errorAlgo deu errado. Verifique phone.error para ver a mensagem. O estado não é limpo sozinho -- chamar connect() novamente inicia uma nova tentativa e redefine o erro.

Exemplos

Com controle de silenciamento

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>
  )
}

Com toque

Reproduza um som de toque durante a conexão para simular uma chamada 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}
    </>
  )
}

O toque é repetido durante o estado connecting e diminui gradualmente quando o agente se conecta. Passe true para usar o toque padrão integrado ou uma string de URL para usar seu próprio arquivo de áudio.

Com 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}
    </>
  )
}

Interface personalizada completa

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>
  )
}

Dicas

Sempre renderize phone.audio

O elemento phone.audio é invisível, mas obrigatório. Coloque-o em qualquer lugar do seu JSX -- ele não renderiza nenhum DOM visível, mas gerencia internamente a conexão de áudio WebRTC.

Desabilite o botão durante a conexão

O estado connecting pode durar de 1 a 3 segundos. Desabilite o botão de chamada durante esse estado para evitar tentativas de conexão duplicadas.

Trate o estado de erro adequadamente

Quando o estado for error, exiba phone.error para a pessoa usuária e mantenha o botão de chamada habilitado. O hook não sai do estado error por conta própria -- chamar connect() novamente inicia uma nova tentativa e limpa o erro anterior.

Use callbacks para efeitos colaterais

Os callbacks onConnect, onDisconnect e onError são ideais para analytics, logs ou para acionar outra lógica da aplicação sem consultar o estado continuamente.

Leia os níveis de áudio de audioLevelRef

audioLevelRef é a única fonte ativa de nível de áudio. Leia audioLevelRef.current dentro de requestAnimationFrame para animações suaves, como formas de onda (ler uma ref não causa novas renderizações), ou faça amostragens em intervalos e armazene o resultado no estado para uma UI renderizada pelo React. O número audioLevel está obsoleto e é sempre 0 -- não crie lógica baseada nele.