Headless Hook
Bouw een volledig aangepaste spraakinterface met de React-hook useThunderPhone
Met de hook useThunderPhone heb je volledige controle over de gebruikersinterface, terwijl ThunderPhone de spraaksessie, audioroutering en verbindingsstatus beheert. Gebruik deze wanneer je een volledig aangepaste UI wilt -- je eigen knoppen, lay-outs, animaties en branding -- terwijl ThunderPhone alles achter de schermen afhandelt.
Wanneer gebruik je de headless-hook
De vooraf gebouwde component ThunderPhoneWidget dekt de meeste gebruiksscenario's, maar gebruik de headless-hook wanneer je het volgende nodig hebt:
- Een volledig aangepaste oproep-UI die aansluit bij het ontwerpsysteem van je app
- Audio-reactieve visualisaties (golfvormen, bollen, pulserende indicatoren) aangestuurd door realtime audioniveaus
- Aangepaste oproepflows, zoals formulieren vóór een oproep, enquêtes na een oproep of inline chat naast spraak
- Integratie in een bestaande componentbibliotheek (Material UI, Chakra, Radix, enz.)
Installatie
npm install @thunderphone/widgetBasisgebruik
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}
</>
)
}Opties
Geef deze opties via UseThunderPhoneOptions door aan useThunderPhone:
| Optie | Type | Vereist | Standaard | Beschrijving |
|---|---|---|---|---|
publishableKey | string | Ja | -- | Publiceerbare API-sleutel (pk_live_...). De agent wordt automatisch bepaald op basis van de widgetconfiguratie van de sleutel. |
apiBase | string | Nee | 'https://api.thunderphone.com/v1' | Overschrijving van de basis-URL voor de API. |
language | string | Nee | -- | Overschrijving van de taal per sessie -- een taalcode of landinstelling zoals en, es of fr-FR. Indien niet ingesteld, wordt de geconfigureerde taal van de agent gebruikt. |
voice | string | Nee | -- | Overschrijving van de stem per sessie -- een stemnaam zoals maria. Indien niet ingesteld, wordt de geconfigureerde stem van de agent gebruikt. |
context | string | Nee | -- | Feitelijke pagina- of sitecontext per sessie die aan de agent wordt doorgegeven. Aan de serverzijde afgekapt tot 12.000 tekens. |
onConnect | () => void | Nee | -- | Aangeroepen wanneer de spraaksessie verbinding maakt. |
onDisconnect | () => void | Nee | -- | Aangeroepen wanneer de sessie eindigt. |
onError | (error) => void | Nee | -- | Aangeroepen bij fouten. Error heeft de velden error (code) en message. |
ringtone | boolean | string | Nee | false | Speel een beltoon af tijdens het verbinden. true voor de standaardbeltoon, of een URL-tekenreeks voor aangepaste audio. |
Retourwaarde
De hook retourneert een UseThunderPhoneReturn-object:
| Eigenschap | Type | Beschrijving |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Huidige verbindingsstatus. |
connect | () => void | Start een spraaksessie. |
disconnect | () => void | Beëindig de huidige sessie. |
toggleMute | () => void | Schakel de microfoondemping in of uit. |
isMuted | boolean | Of de microfoon momenteel gedempt is. |
error | string | undefined | Foutmelding wanneer de status 'error' is. |
agentName | string | undefined | Weergavenaam van de verbonden agent. |
audioLevel | number | Verouderd -- altijd 0. Een statische tijdelijke waarde die behouden blijft voor achterwaartse compatibiliteit; deze wordt nooit bijgewerkt. Lees in plaats daarvan audioLevelRef.current. |
audioLevelRef | React.RefObject<number> | Een wijzigbare ref met het realtime-audioniveau (0--1) -- het hoogste niveau van de stem van de agent en de microfoon van de bezoeker -- die bij elk animatieframe wordt bijgewerkt, buiten de rendercyclus van React. Lees audioLevelRef.current binnen requestAnimationFrame-lussen voor vloeiende animaties zonder haperingen, of meet deze met een interval wanneer je de waarde in de React-status nodig hebt. |
audio | ReactNode | Onzichtbaar element dat de audioverbinding afhandelt -- moet worden gerenderd. |
Audio-reactieve UI
De ref audioLevelRef geeft je audiolevels met framerate zonder React-herweergaven te activeren, waardoor deze ideaal is voor vloeiende golfvormvisualisaties, pulserende bollen of elke animatie die aan het gesprek is gekoppeld. Het level weerspiegelt wat het hardst is: de stem van de agent of de microfoon van de bezoeker.
Voorbeeld van een golfvorm
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>
)
}Voorbeeld van een pulserende bol
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>
)
}Voorbeeld van een spreekindicator
Voor een door React gerenderde UI die met het volume verandert -- zoals een op drempelwaarde gebaseerd label voor "spreken" -- lees je audioLevelRef.current periodiek uit en sla je het resultaat op in de state:
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>
)
}Toestandsmachine
De eigenschap state volgt deze levenscyclus:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Status | Beschrijving |
|---|---|
idle | Geen actieve sessie. Klaar om connect() aan te roepen. |
connecting | De sessie wordt tot stand gebracht. Schakel de belknop uit tijdens deze status. |
connected | De spraaksessie is actief. De gebruiker praat met de agent. |
disconnected | De sessie is correct beëindigd. Keert na 1,5 seconde automatisch terug naar idle. |
error | Er is iets misgegaan. Controleer phone.error voor het bericht. De status wordt niet vanzelf gewist -- door connect() opnieuw aan te roepen start je een nieuwe poging en wordt de fout gereset. |
Voorbeelden
Met dempbediening
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>
)
}Met beltoon
Speel een belgeluid af tijdens het verbinden om een telefoongesprek na te bootsen:
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}
</>
)
}De beltoon wordt herhaald tijdens de status connecting en vervaagt wanneer de agent verbinding maakt. Geef true door voor de ingebouwde standaardbeltoon, of een URL-tekenreeks om je eigen audiobestand te gebruiken.
Met event-callbacks
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}
</>
)
}Volledig aangepaste UI
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>
)
}Tips
phone.audio altijd renderen
Het element phone.audio is onzichtbaar maar vereist. Plaats het ergens in je JSX -- het rendert geen zichtbare DOM, maar beheert intern de WebRTC-audioverbinding.
De knop uitschakelen tijdens het verbinden
De status connecting kan 1-3 seconden duren. Schakel de oproepknop tijdens deze status uit om dubbele verbindingspogingen te voorkomen.
De foutstatus zorgvuldig afhandelen
Wanneer de status error is, toon je phone.error aan de gebruiker en houd je oproepknop ingeschakeld. De hook verlaat de status error niet vanzelf -- door connect() opnieuw aan te roepen start je een nieuwe poging en wordt de vorige fout gewist.
Callbacks gebruiken voor neveneffecten
De callbacks onConnect, onDisconnect en onError zijn ideaal voor analytics, logging of het activeren van andere applicatielogica zonder de status te pollen.
Audioniveaus lezen uit audioLevelRef
audioLevelRef is de enige livebron voor audioniveaus. Lees audioLevelRef.current binnen requestAnimationFrame voor vloeiende animaties zoals golfvormen (het lezen van een ref veroorzaakt geen re-renders), of bemonster het met een interval en sla het resultaat op in state voor door React gerenderde UI. Het getal audioLevel is verouderd en altijd 0 -- bouw er geen logica op.