Headless Hook
Byg en fuldt tilpasset stemmegrænseflade med React-hooken useThunderPhone
Hooken useThunderPhone giver dig fuld kontrol over brugergrænsefladen, mens ThunderPhone håndterer stemmesessionen, lydrouting og forbindelsestilstand. Brug den, når du vil have et fuldt tilpasset UI -- dine egne knapper, layouts, animationer og branding -- mens ThunderPhone håndterer alt i baggrunden.
Hvornår du skal bruge headless-hooken
Den færdigbyggede komponent ThunderPhoneWidget dækker de fleste anvendelsestilfælde, men brug headless-hooken, når du har brug for:
- Et helt tilpasset opkalds-UI, der matcher din apps designsystem
- Lydreaktive visualiseringer (bølgeformer, kugler, pulserende indikatorer) drevet af lydniveauer i realtid
- Tilpassede opkaldsforløb såsom formularer før opkaldet, spørgeundersøgelser efter opkaldet eller integreret chat ved siden af stemmen
- Integration i et eksisterende komponentbibliotek (Material UI, Chakra, Radix osv.)
Installation
npm install @thunderphone/widgetGrundlæggende brug
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}
</>
)
}Indstillinger
Send disse indstillinger til useThunderPhone via UseThunderPhoneOptions:
| Indstilling | Type | Påkrævet | Standard | Beskrivelse |
|---|---|---|---|---|
publishableKey | string | Ja | -- | Offentlig API-nøgle (pk_live_...). Agenten bestemmes automatisk ud fra nøglens widgetkonfiguration. |
apiBase | string | Nej | 'https://api.thunderphone.com/v1' | Tilsidesættelse af API-basis-URL. |
language | string | Nej | -- | Tilsidesættelse af sprog pr. session -- en sprogkode eller lokalitet som en, es eller fr-FR. Når den ikke er angivet, bruges agentens konfigurerede sprog. |
voice | string | Nej | -- | Tilsidesættelse af stemme pr. session -- et stemmenavn som maria. Når den ikke er angivet, bruges agentens konfigurerede stemme. |
context | string | Nej | -- | Faktuel side- eller sitekontekst pr. session, der sendes til agenten. Afkortes på serversiden til 12.000 tegn. |
onConnect | () => void | Nej | -- | Kaldes, når stemmesessionen opretter forbindelse. |
onDisconnect | () => void | Nej | -- | Kaldes, når sessionen afsluttes. |
onError | (error) => void | Nej | -- | Kaldes ved fejl. Fejlen har felterne error (kode) og message. |
ringtone | boolean | string | Nej | false | Afspil en ringetone under forbindelsen. true for standardringetonen eller en URL-streng til brugerdefineret lyd. |
Returværdi
Hooket returnerer et UseThunderPhoneReturn-objekt:
| Egenskab | Type | Beskrivelse |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Aktuel forbindelsestilstand. |
connect | () => void | Start en stemmesession. |
disconnect | () => void | Afslut den aktuelle session. |
toggleMute | () => void | Slå mikrofonens mute til eller fra. |
isMuted | boolean | Om mikrofonen aktuelt er muted. |
error | string | undefined | Fejlmeddelelse, når tilstanden er 'error'. |
agentName | string | undefined | Vist navn på den tilsluttede agent. |
audioLevel | number | Forældet -- altid 0. En statisk pladsholder, der bevares for bagudkompatibilitet; den opdateres aldrig. Læs audioLevelRef.current i stedet. |
audioLevelRef | React.RefObject<number> | En muterbar ref, der indeholder lydniveauet i realtid (0--1) -- det højeste af agentens stemme og den besøgendes mikrofon -- opdateret på hvert animationsframe uden for Reacts renderingscyklus. Læs audioLevelRef.current i requestAnimationFrame-løkker for jævne animationer uden hakken, eller aflæs den med et interval, når du har brug for værdien i React-state. |
audio | ReactNode | Usynligt element, der håndterer lydforbindelsen -- skal renderes. |
Lydreaktivt brugerinterface
Ref'en audioLevelRef giver dig lydniveauer ved billedfrekvens uden at udloese React-genrenderinger, hvilket goer den ideel til at styre glatte boelgeformsvisualiseringer, pulserende kugler eller enhver animation, der er knyttet til samtalen. Niveauet afspejler den lyd, der er hoejest: agentens stemme eller besoegendes mikrofon.
Eksempel paa boelgeform
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 paa pulserende kugle
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 paa taleindikator
For React-gengivet brugerinterface, der aendrer sig med lydstyrken -- som et maerkat for "taler" baseret paa en taerskelvaerdi -- skal du maale audioLevelRef.current med et interval og gemme resultatet i 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>
)
}Tilstandsmaskine
Egenskaben state følger denne livscyklus:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Tilstand | Beskrivelse |
|---|---|
idle | Ingen aktiv session. Klar til at kalde connect(). |
connecting | Sessionen oprettes. Deaktiver opkaldsknappen i denne tilstand. |
connected | Stemmesessionen er aktiv. Brugeren taler med agenten. |
disconnected | Sessionen er afsluttet korrekt. Skifter automatisk tilbage til idle efter 1,5 sekunder. |
error | Noget gik galt. Se phone.error for meddelelsen. Tilstanden ryddes ikke af sig selv -- når du kalder connect() igen, starter det et nyt forsøg og nulstiller fejlen. |
Eksempler
Med lydstyring
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
Afspil en ringelyd under forbindelsen for at simulere et telefonopkald:
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 afspilles i sløjfe under tilstanden connecting og toner ud, når agenten forbindes. Angiv true for den indbyggede standardringetone eller en URL-streng for at bruge din egen lydfil.
Med hændelseskald
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}
</>
)
}Fuldt tilpasset brugerflade
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 altid phone.audio
Elementet phone.audio er usynligt, men påkrævet. Placer det hvor som helst i din JSX -- det renderer ingen synlig DOM, men administrerer WebRTC-lydforbindelsen internt.
Deaktiver knappen under tilslutning
Tilstanden connecting kan vare 1-3 sekunder. Deaktiver opkaldsknappen i denne tilstand for at forhindre dublerede forbindelsesforsøg.
Håndter fejltilstanden elegant
Når tilstanden er error, skal du vise phone.error til brugeren og holde opkaldsknappen aktiveret. Hooken forlader ikke selv tilstanden error -- et nyt kald til connect() starter et nyt forsøg og rydder den forrige fejl.
Brug callbacks til sideeffekter
Callbacksene onConnect, onDisconnect og onError er ideelle til analyse, logføring eller til at udløse anden applikationslogik uden at polle tilstanden.
Læs lydniveauer fra audioLevelRef
audioLevelRef er den eneste kilde til live-lydniveauer. Læs audioLevelRef.current inde i requestAnimationFrame for jævne animationer som bølgeformer (at læse en ref medfører ikke genrenderinger), eller udtag prøver med et interval og gem resultatet i state for React-renderet brugergrænseflade. Tallet audioLevel er forældet og altid 0 -- byg ikke logik på det.