ThunderPhone 2.0 est disponible.En libre-service, à partir de 2 ¢/min.Découvrir l’annonce

Widget

Hook headless

Créez une interface vocale entièrement personnalisée avec le hook React useThunderPhone

Le hook useThunderPhone vous donne un contrôle total sur l'interface utilisateur tandis que ThunderPhone gère la session vocale, le routage audio et l'état de connexion. Utilisez-le lorsque vous souhaitez une UI entièrement personnalisée -- vos propres boutons, mises en page, animations et identité visuelle -- tandis que ThunderPhone gère tout en coulisses.

Quand utiliser le hook headless

Le composant préconstruit ThunderPhoneWidget couvre la plupart des cas d'utilisation, mais utilisez le hook headless lorsque vous avez besoin de :

  • Une UI d'appel entièrement personnalisée qui correspond au système de design de votre application
  • Visualisations réactives à l'audio (formes d'onde, orbes, indicateurs pulsés) pilotées par les niveaux audio en temps réel
  • Flux d'appel personnalisés, tels que des formulaires avant appel, des enquêtes après appel ou un chat intégré à côté de la voix
  • Intégration dans une bibliothèque de composants existante (Material UI, Chakra, Radix, etc.)

Installation

npm install @thunderphone/widget

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

Options

Transmettez ces options à useThunderPhone via UseThunderPhoneOptions :

OptionTypeObligatoirePar défautDescription
publishableKeystringOui--Clé API publiable (pk_live_...). L'agent vocal est automatiquement déterminé à partir de la configuration du widget associée à la clé.
apiBasestringNon'https://api.thunderphone.com/v1'Remplacement de l'URL de base de l'API.
languagestringNon--Remplacement de la langue par session -- un code de langue ou une locale telle que en, es ou fr-FR. Lorsqu'elle n'est pas définie, la langue configurée de l'agent vocal s'applique.
voicestringNon--Remplacement de la voix par session -- un nom de voix tel que maria. Lorsqu'elle n'est pas définie, la voix configurée de l'agent vocal s'applique.
contextstringNon--Contexte factuel de page ou de site transmis à l'agent vocal pour chaque session. Tronqué côté serveur à 12 000 caractères.
onConnect() => voidNon--Appelé lorsque la session vocale se connecte.
onDisconnect() => voidNon--Appelé lorsque la session se termine.
onError(error) => voidNon--Appelé en cas d'erreur. L'erreur contient les champs error (code) et message.
ringtoneboolean | stringNonfalseJoue une sonnerie pendant la connexion. true pour la sonnerie par défaut, ou une chaîne d'URL pour un audio personnalisé.

Valeur de retour

Le hook renvoie un objet UseThunderPhoneReturn :

PropriétéTypeDescription
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'État actuel de la connexion.
connect() => voidDémarrer une session vocale.
disconnect() => voidTerminer la session actuelle.
toggleMute() => voidActiver ou désactiver le mode muet du microphone.
isMutedbooleanIndique si le microphone est actuellement désactivé.
errorstring | undefinedMessage d’erreur lorsque l’état est 'error'.
agentNamestring | undefinedNom d’affichage de l’agent connecté.
audioLevelnumberObsolète -- toujours 0. Espace réservé statique conservé pour la rétrocompatibilité ; il n’est jamais mis à jour. Lisez plutôt audioLevelRef.current.
audioLevelRefReact.RefObject<number>Une ref mutable contenant le niveau audio en temps réel (0--1) -- le plus élevé entre la voix de l’agent et le microphone du visiteur -- mise à jour à chaque image d’animation, en dehors du cycle de rendu de React. Lisez audioLevelRef.current dans les boucles requestAnimationFrame pour des animations fluides et sans saccades, ou échantillonnez-la à intervalles réguliers lorsque vous avez besoin de la valeur dans l’état React.
audioReactNodeÉlément invisible qui gère la connexion audio -- doit être rendu.

Interface réactive à l’audio

La ref audioLevelRef vous fournit des niveaux audio à la fréquence d’images sans déclencher de nouveaux rendus React, ce qui la rend idéale pour piloter des visualisations de forme d’onde fluides, des orbes pulsantes ou toute animation liée à la conversation. Le niveau reflète la source la plus forte : la voix de l’agent vocal ou le microphone du visiteur.

Exemple de forme d’onde

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

Exemple d’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>
  )
}

Exemple d’indicateur de parole

Pour une interface rendue par React qui évolue avec le volume — par exemple un badge « speaking » basé sur un seuil — échantillonnez audioLevelRef.current à intervalles réguliers et stockez le résultat dans l’état :

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

Machine à états

La propriété state suit ce cycle de vie :

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
ÉtatDescription
idleAucune session active. Prêt à appeler connect().
connectingLa session est en cours d’établissement. Désactivez le bouton d’appel pendant cet état.
connectedLa session vocale est active. L’utilisateur parle à l’agent.
disconnectedLa session s’est terminée correctement. Repasse automatiquement à idle après 1,5 seconde.
errorUn problème est survenu. Consultez phone.error pour voir le message. L’état ne s’efface pas de lui-même -- appeler à nouveau connect() lance une nouvelle tentative et réinitialise l’erreur.

Exemples

Avec contrôle du micro

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

Avec sonnerie

Jouez une sonnerie pendant la connexion afin de simuler un appel téléphonique :

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 sonnerie se répète pendant l’état connecting et s’estompe lorsque l’agent se connecte. Transmettez true pour utiliser la sonnerie par défaut intégrée, ou une chaîne d’URL pour utiliser votre propre fichier audio.

Avec callbacks d’événements

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 utilisateur entièrement personnalisée

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

Conseils

Toujours rendre phone.audio

L’élément phone.audio est invisible, mais requis. Placez-le n’importe où dans votre JSX -- il ne rend aucun DOM visible, mais gère la connexion audio WebRTC en interne.

Désactiver le bouton pendant la connexion

L’état connecting peut durer de 1 à 3 secondes. Désactivez le bouton d’appel pendant cet état afin d’éviter les tentatives de connexion en double.

Gérer l’état d’erreur correctement

Lorsque l’état est error, affichez phone.error à l’utilisateur et maintenez votre bouton d’appel activé. Le hook ne quitte pas seul l’état error -- appeler à nouveau connect() lance une nouvelle tentative et efface l’erreur précédente.

Utiliser des callbacks pour les effets secondaires

Les callbacks onConnect, onDisconnect et onError sont idéaux pour l’analytique, la journalisation ou le déclenchement d’une autre logique applicative sans interroger l’état.

Lire les niveaux audio depuis audioLevelRef

audioLevelRef est la seule source de niveau audio en direct. Lisez audioLevelRef.current dans requestAnimationFrame pour des animations fluides telles que des formes d’onde (la lecture d’une ref ne provoque pas de nouveaux rendus), ou échantillonnez-le à intervalles réguliers et stockez le résultat dans l’état pour une interface rendue par React. Le nombre audioLevel est obsolète et vaut toujours 0 -- ne construisez pas de logique dessus.