Headless Hook
Bygg et fullstendig tilpasset stemmegrensesnitt med React-hooken useThunderPhone
useThunderPhone-hooken gir deg full kontroll over brukergrensesnittet mens ThunderPhone håndterer stemmeøkten, lydrutingen og tilkoblingsstatusen. Bruk den når du vil ha et helt tilpasset brukergrensesnitt -- egne knapper, oppsett, animasjoner og profilering -- mens ThunderPhone håndterer alt i bakgrunnen.
Når du skal bruke Headless-hooken
Den ferdigbygde ThunderPhoneWidget-komponenten dekker de fleste brukstilfeller, men bruk headless-hooken når du trenger:
- Et helt tilpasset samtalegrensesnitt som matcher designsystemet i appen din
- Lydreaktive visualiseringer (bølgeformer, kuler, pulserende indikatorer) drevet av lydnivåer i sanntid
- Tilpassede samtaleflyter som skjemaer før samtalen, undersøkelser etter samtalen eller innebygd chat ved siden av stemmen
- Integrasjon i et eksisterende komponentbibliotek (Material UI, Chakra, Radix osv.)
Installasjon
npm install @thunderphone/widgetGrunnleggende bruk
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}
</>
)
}Alternativer
Send disse alternativene til useThunderPhone via UseThunderPhoneOptions:
| Alternativ | Type | Påkrevd | Standard | Beskrivelse |
|---|---|---|---|---|
publishableKey | string | Ja | -- | Publiserbar API-nøkkel (pk_live_...). Stemmeagenten bestemmes automatisk fra nøkkelens widgetkonfigurasjon. |
apiBase | string | Nei | 'https://api.thunderphone.com/v1' | Overstyring av API-basis-URL. |
language | string | Nei | -- | Språkoverstyring per økt -- en språkkode eller lokalitet som en, es eller fr-FR. Når den ikke er angitt, brukes stemmeagentens konfigurerte språk. |
voice | string | Nei | -- | Stemmeoverstyring per økt -- et stemmenavn som maria. Når den ikke er angitt, brukes stemmeagentens konfigurerte stemme. |
context | string | Nei | -- | Faktabasert side- eller nettstedskontekst per økt som sendes til stemmeagenten. Avkortes på serversiden til 12 000 tegn. |
onConnect | () => void | Nei | -- | Kalles når stemmeøkten kobles til. |
onDisconnect | () => void | Nei | -- | Kalles når økten avsluttes. |
onError | (error) => void | Nei | -- | Kalles ved feil. Feilen har feltene error (kode) og message. |
ringtone | boolean | string | Nei | false | Spill av en ringetone mens tilkoblingen opprettes. true for standardringetonen, eller en URL-streng for egendefinert lyd. |
Returverdi
Hooken returnerer et UseThunderPhoneReturn-objekt:
| Egenskap | Type | Beskrivelse |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Gjeldende tilkoblingsstatus. |
connect | () => void | Start en stemmeøkt. |
disconnect | () => void | Avslutt gjeldende økt. |
toggleMute | () => void | Slå mikrofonens demping av/på. |
isMuted | boolean | Om mikrofonen er dempet nå. |
error | string | undefined | Feilmelding når statusen er 'error'. |
agentName | string | undefined | Visningsnavn for den tilkoblede agenten. |
audioLevel | number | Utdatert -- alltid 0. En statisk plassholder beholdt for bakoverkompatibilitet; den oppdateres aldri. Les audioLevelRef.current i stedet. |
audioLevelRef | React.RefObject<number> | En muterbar ref som inneholder lydnivået i sanntid (0--1) -- det høyeste av stemmeagentens stemme og den besøkendes mikrofon -- oppdatert på hver animasjonsramme, utenfor Reacts rendringssyklus. Les audioLevelRef.current i requestAnimationFrame-løkker for jevne animasjoner uten hakking, eller hent verdien med et intervall når du trenger den i React-status. |
audio | ReactNode | Usynlig element som håndterer lydtilkoblingen -- må rendres. |
Lydreaktivt brukergrensesnitt
Ref-en audioLevelRef gir deg lydnivåer med bildefrekvens uten å utløse React-gjengivelser, noe som gjør den ideell for jevne bølgeformvisualiseringer, pulserende kuler eller enhver animasjon knyttet til samtalen. Nivået gjenspeiler det som er høyest: agentens stemme eller den besøkendes mikrofon.
Eksempel på bølgeform
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>
)
}Eksempel på pulserende kule
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>
)
}Eksempel på taleindikator
For React-gjengitt brukergrensesnitt som endres med volumet -- for eksempel et terskelbasert «snakker»-merke -- les audioLevelRef.current med et intervall og lagre resultatet i tilstand:
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>
)
}Tilstandsmaskin
Egenskapen state følger denne livssyklusen:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Tilstand | Beskrivelse |
|---|---|
idle | Ingen aktiv økt. Klar til å kalle connect(). |
connecting | Økten opprettes. Deaktiver ringeknappen i denne tilstanden. |
connected | Stemmeøkten er aktiv. Brukeren snakker med agenten. |
disconnected | Økten er avsluttet på en ryddig måte. Går automatisk tilbake til idle etter 1,5 sekunder. |
error | Noe gikk galt. Sjekk phone.error for meldingen. Tilstanden tømmes ikke av seg selv -- å kalle connect() på nytt starter et nytt forsøk og tilbakestiller feilen. |
Eksempler
Med dempekontroll
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>
)
}Med ringetone
Spill av en ringelyd mens tilkoblingen opprettes for å simulere en telefonsamtale:
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}
</>
)
}Ringetonen gjentas mens statusen er connecting og tones ut når stemmeagenten kobler til. Send inn true for den innebygde standardringetonen, eller en URL-streng for å bruke din egen lydfil.
Med hendelsestilbakekall
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}
</>
)
}Fullt tilpasset brukergrensesnitt
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
Render alltid phone.audio
Elementet phone.audio er usynlig, men nødvendig. Plasser det hvor som helst i JSX-en din -- det renderer ingen synlig DOM, men håndterer WebRTC-lydtilkoblingen internt.
Deaktiver knappen mens tilkoblingen opprettes
Tilstanden connecting kan vare i 1–3 sekunder. Deaktiver ringeknappen i denne tilstanden for å forhindre doble tilkoblingsforsøk.
Håndter feiltilstanden på en god måte
Når tilstanden er error, vis phone.error til brukeren og hold ringeknappen aktivert. Hooken forlater ikke error-tilstanden av seg selv -- å kalle connect() på nytt starter et nytt forsøk og fjerner den forrige feilen.
Bruk tilbakeringinger for sideeffekter
Tilbakeringingene onConnect, onDisconnect og onError er ideelle for analyse, logging eller utløsing av annen applikasjonslogikk uten å polle tilstanden.
Les lydnivåer fra audioLevelRef
audioLevelRef er den eneste direkte kilden til lydnivå. Les audioLevelRef.current inne i requestAnimationFrame for jevne animasjoner som bølgeformer (å lese en ref forårsaker ikke ny rendering), eller hent en prøve med jevne mellomrom og lagre resultatet i tilstanden for React-rendert brukergrensesnitt. Tallet audioLevel er utdatert og alltid 0 -- ikke bygg logikk på det.