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/widgetUtilisation 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 :
| Option | Type | Obligatoire | Par défaut | Description |
|---|---|---|---|---|
publishableKey | string | Oui | -- | Clé API publiable (pk_live_...). L'agent vocal est automatiquement déterminé à partir de la configuration du widget associée à la clé. |
apiBase | string | Non | 'https://api.thunderphone.com/v1' | Remplacement de l'URL de base de l'API. |
language | string | Non | -- | 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. |
voice | string | Non | -- | 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. |
context | string | Non | -- | Contexte factuel de page ou de site transmis à l'agent vocal pour chaque session. Tronqué côté serveur à 12 000 caractères. |
onConnect | () => void | Non | -- | Appelé lorsque la session vocale se connecte. |
onDisconnect | () => void | Non | -- | Appelé lorsque la session se termine. |
onError | (error) => void | Non | -- | Appelé en cas d'erreur. L'erreur contient les champs error (code) et message. |
ringtone | boolean | string | Non | false | Joue 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é | Type | Description |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | État actuel de la connexion. |
connect | () => void | Démarrer une session vocale. |
disconnect | () => void | Terminer la session actuelle. |
toggleMute | () => void | Activer ou désactiver le mode muet du microphone. |
isMuted | boolean | Indique si le microphone est actuellement désactivé. |
error | string | undefined | Message d’erreur lorsque l’état est 'error'. |
agentName | string | undefined | Nom d’affichage de l’agent connecté. |
audioLevel | number | Obsolète -- toujours 0. Espace réservé statique conservé pour la rétrocompatibilité ; il n’est jamais mis à jour. Lisez plutôt audioLevelRef.current. |
audioLevelRef | React.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. |
audio | ReactNode | É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)
| État | Description |
|---|---|
idle | Aucune session active. Prêt à appeler connect(). |
connecting | La session est en cours d’établissement. Désactivez le bouton d’appel pendant cet état. |
connected | La session vocale est active. L’utilisateur parle à l’agent. |
disconnected | La session s’est terminée correctement. Repasse automatiquement à idle après 1,5 seconde. |
error | Un 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.