ThunderPhone 2.0 è arrivato.Parti in autonomia, da 2¢/min.Leggi l’annuncio

Widget

Hook headless

Crea un

L'hook useThunderPhone ti offre il controllo completo sull'interfaccia utente mentre ThunderPhone gestisce la sessione vocale, il routing audio e lo stato della connessione. Usalo quando vuoi una UI completamente personalizzata -- con pulsanti, layout, animazioni e branding propri -- mentre ThunderPhone gestisce tutto dietro le quinte.

Quando usare l'hook Headless

Il componente predefinito ThunderPhoneWidget copre la maggior parte dei casi d'uso, ma usa l'hook headless quando ti serve:

  • Un'interfaccia di chiamata completamente personalizzata che corrisponda al design system della tua app
  • Visualizzazioni reattive all'audio (forme d'onda, sfere, indicatori pulsanti) basate sui livelli audio in tempo reale
  • Flussi di chiamata personalizzati, come moduli prima della chiamata, sondaggi dopo la chiamata o chat inline accanto alla voce
  • Integrazione in una libreria di componenti esistente (Material UI, Chakra, Radix, ecc.)

Installazione

npm install @thunderphone/widget

Utilizzo di base

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

Opzioni

Passa queste opzioni a useThunderPhone tramite UseThunderPhoneOptions:

OpzioneTipoObbligatorioPredefinitoDescrizione
publishableKeystring--Chiave API pubblicabile (pk_live_...). L'agente viene risolto automaticamente dalla configurazione del widget della chiave.
apiBasestringNo'https://api.thunderphone.com/v1'Sostituzione dell'URL di base dell'API.
languagestringNo--Sostituzione della lingua per sessione -- un codice lingua o una lingua locale come en, es o fr-FR. Se non impostata, viene applicata la lingua configurata dell'agente.
voicestringNo--Sostituzione della voce per sessione -- un nome di voce come maria. Se non impostata, viene applicata la voce configurata dell'agente.
contextstringNo--Contesto fattuale della pagina o del sito per sessione passato all'agente. Troncato lato server a 12.000 caratteri.
onConnect() => voidNo--Chiamata quando la sessione vocale si connette.
onDisconnect() => voidNo--Chiamata al termine della sessione.
onError(error) => voidNo--Chiamata in caso di errori. L'errore ha i campi error (codice) e message.
ringtoneboolean | stringNofalseRiproduce una suoneria durante la connessione. true per la suoneria predefinita oppure una stringa URL per un audio personalizzato.

Valore restituito

L'hook restituisce un oggetto UseThunderPhoneReturn:

ProprietàTipoDescrizione
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Stato attuale della connessione.
connect() => voidAvvia una sessione vocale.
disconnect() => voidTermina la sessione corrente.
toggleMute() => voidAttiva o disattiva il silenziamento del microfono.
isMutedbooleanIndica se il microfono è attualmente silenziato.
errorstring | undefinedMessaggio di errore quando lo stato è 'error'.
agentNamestring | undefinedNome visualizzato dell'agente connesso.
audioLevelnumberDeprecato -- sempre 0. Un segnaposto statico mantenuto per la retrocompatibilità; non si aggiorna mai. Leggi invece audioLevelRef.current.
audioLevelRefReact.RefObject<number>Un ref mutabile che contiene il livello audio in tempo reale (0--1) -- il più alto tra la voce dell'agente e il microfono del visitatore -- aggiornato a ogni frame di animazione, al di fuori del ciclo di rendering di React. Leggi audioLevelRef.current all'interno dei loop requestAnimationFrame per animazioni fluide e senza scatti, oppure campionalo a intervalli quando ti serve il valore nello stato di React.
audioReactNodeElemento invisibile che gestisce la connessione audio -- deve essere renderizzato.

Interfaccia utente reattiva all'audio

La ref audioLevelRef fornisce livelli audio al frame rate senza attivare nuovi rendering di React, risultando ideale per visualizzazioni di forme d'onda fluide, sfere pulsanti o qualsiasi animazione collegata alla conversazione. Il livello riflette la sorgente più alta tra la voce dell'agente e il microfono del visitatore.

Esempio di forma d'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>
  )
}

Esempio di sfera 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>
  )
}

Esempio di indicatore di conversazione

Per un'interfaccia utente renderizzata da React che cambia in base al volume -- ad esempio un badge "sta parlando" basato su una soglia -- campiona audioLevelRef.current a intervalli e salva il risultato nello stato:

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

Macchina a stati

La proprietà state segue questo ciclo di vita:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
StatoDescrizione
idleNessuna sessione attiva. Pronto a chiamare connect().
connectingLa sessione è in fase di avvio. Disabilita il pulsante di chiamata durante questo stato.
connectedLa sessione vocale è attiva. L'utente sta parlando con l'agente.
disconnectedLa sessione si è conclusa correttamente. Torna automaticamente a idle dopo 1,5 secondi.
errorSi è verificato un problema. Controlla phone.error per il messaggio. Lo stato non si cancella da solo -- chiamare di nuovo connect() avvia un nuovo tentativo e reimposta l'errore.

Esempi

Con controllo del microfono

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 suoneria

Riproduci un suono di squillo durante la connessione per simulare una chiamata telefonica:

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

La suoneria viene riprodotta in loop durante lo stato connecting e sfuma quando l'agente si connette. Passa true per usare la suoneria predefinita integrata oppure una stringa URL per usare il tuo file audio.

Con callback degli eventi

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

Interfaccia utente completamente personalizzata

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

Suggerimenti

Esegui sempre il rendering di phone.audio

L'elemento phone.audio è invisibile ma obbligatorio. Inseriscilo ovunque nel tuo JSX -- non esegue il rendering di alcun DOM visibile, ma gestisce internamente la connessione audio WebRTC.

Disabilita il pulsante durante la connessione

Lo stato connecting può durare 1-3 secondi. Disabilita il pulsante di chiamata durante questo stato per evitare tentativi di connessione duplicati.

Gestisci correttamente lo stato di errore

Quando lo stato è error, mostra phone.error all'utente e mantieni abilitato il pulsante di chiamata. L'hook non esce autonomamente dallo stato error -- chiamare di nuovo connect() avvia un nuovo tentativo e cancella l'errore precedente.

Usa i callback per gli effetti collaterali

I callback onConnect, onDisconnect e onError sono ideali per analisi, registrazione dei log o per attivare altra logica dell'applicazione senza eseguire il polling dello stato.

Leggi i livelli audio da audioLevelRef

audioLevelRef è l'unica fonte live dei livelli audio. Leggi audioLevelRef.current all'interno di requestAnimationFrame per animazioni fluide come le forme d'onda (la lettura di una ref non causa nuovi rendering), oppure campionalo a intervalli e memorizza il risultato nello stato per un'interfaccia utente renderizzata da React. Il numero audioLevel è deprecato ed è sempre 0 -- non basare alcuna logica su di esso.