Headless Hook
Bygg ett helt anpassat röstgränssnitt med React-hooken useThunderPhone
Hooken useThunderPhone ger dig fullständig kontroll över användargränssnittet medan ThunderPhone hanterar röstsessionen, ljudroutningen och anslutningsstatusen. Använd den när du vill ha ett helt anpassat UI -- egna knappar, layouter, animationer och varumärkesprofilering -- medan ThunderPhone hanterar allt i bakgrunden.
När du ska använda den headlessa hooken
Den färdiga komponenten ThunderPhoneWidget täcker de flesta användningsfall, men använd den headlessa hooken när du behöver:
- Ett helt anpassat samtals-UI som matchar appens designsystem
- Ljudreaktiva visualiseringar (vågformer, klot, pulserande indikatorer) som styrs av ljudnivåer i realtid
- Anpassade samtalsflöden, till exempel formulär före samtalet, enkäter efter samtalet eller integrerad chatt vid sidan av röst
- Integration i ett befintligt komponentbibliotek (Material UI, Chakra, Radix osv.)
Installation
npm install @thunderphone/widgetGrundläggande användning
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}
</>
)
}Alternativ
Skicka dessa alternativ till useThunderPhone via UseThunderPhoneOptions:
| Alternativ | Typ | Krävs | Standard | Beskrivning |
|---|---|---|---|---|
publishableKey | string | Ja | -- | Publicerbar API-nyckel (pk_live_...). Agenten bestäms automatiskt från nyckelns widgetkonfiguration. |
apiBase | string | Nej | 'https://api.thunderphone.com/v1' | Åsidosättning av API-bas-URL. |
language | string | Nej | -- | Åsidosättning av språk per session -- en språkkod eller språkvariant som en, es eller fr-FR. När den inte är angiven används agentens konfigurerade språk. |
voice | string | Nej | -- | Åsidosättning av röst per session -- ett röstnamn som maria. När den inte är angiven används agentens konfigurerade röst. |
context | string | Nej | -- | Faktabaserad sid- eller webbplatskontext per session som skickas till agenten. Avkortas på serversidan till 12 000 tecken. |
onConnect | () => void | Nej | -- | Anropas när röstsessionen ansluts. |
onDisconnect | () => void | Nej | -- | Anropas när sessionen avslutas. |
onError | (error) => void | Nej | -- | Anropas vid fel. Felet har fälten error (kod) och message. |
ringtone | boolean | string | Nej | false | Spela en ringsignal medan anslutning sker. true för standardringsignalen eller en URL-sträng för anpassat ljud. |
Returvärde
Hooken returnerar ett UseThunderPhoneReturn-objekt:
| Egenskap | Typ | Beskrivning |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Aktuellt anslutningsläge. |
connect | () => void | Starta en röstsession. |
disconnect | () => void | Avsluta den aktuella sessionen. |
toggleMute | () => void | Växla mikrofonens avstängning på/av. |
isMuted | boolean | Om mikrofonen för närvarande är avstängd. |
error | string | undefined | Felmeddelande när läget är 'error'. |
agentName | string | undefined | Visningsnamn för den anslutna agenten. |
audioLevel | number | Utfasad -- alltid 0. En statisk platshållare som behålls för bakåtkompatibilitet; den uppdateras aldrig. Läs audioLevelRef.current i stället. |
audioLevelRef | React.RefObject<number> | En föränderlig ref som innehåller ljudnivån i realtid (0--1) -- den högsta av agentens röst och besökarens mikrofon -- uppdaterad i varje animationsbildruta, utanför Reacts renderingscykel. Läs audioLevelRef.current i requestAnimationFrame-loopar för smidiga animationer utan hack, eller hämta värdet med ett intervall när du behöver det i React-tillståndet. |
audio | ReactNode | Osynligt element som hanterar ljudanslutningen -- måste renderas. |
Ljudreaktivt användargränssnitt
Referensen audioLevelRef ger dig ljudnivåer i bildfrekvens utan att utlösa React-omrenderingar, vilket gör den idealisk för att styra jämna vågformsvisualiseringar, pulserande sfärer eller andra animationer som är kopplade till samtalet. Nivån återspeglar det som är högst: agentens röst eller besökarens mikrofon.
Exempel på vågform
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>
)
}Exempel på pulserande sfär
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>
)
}Exempel på talindikator
För React-renderade användargränssnitt som ändras med volymen -- till exempel en tröskelbaserad indikator för ”talar” -- läser du av audioLevelRef.current med ett intervall och sparar 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>
)
}Tillståndsmaskin
Egenskapen state följer denna livscykel:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Tillstånd | Beskrivning |
|---|---|
idle | Ingen aktiv session. Redo att anropa connect(). |
connecting | Sessionen upprättas. Inaktivera samtalsknappen under detta tillstånd. |
connected | Röstsessionen är aktiv. Användaren pratar med agenten. |
disconnected | Sessionen har avslutats utan fel. Övergår automatiskt tillbaka till idle efter 1,5 sekunder. |
error | Något gick fel. Kontrollera phone.error för meddelandet. Tillståndet rensas inte av sig självt -- att anropa connect() igen startar ett nytt försök och återställer felet. |
Exempel
Med ljudavstängningskontroll
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 ringsignal
Spela upp ett ringsignal-ljud medan anslutningen upprättas för att simulera ett telefonsamtal:
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}
</>
)
}Ringsignalen loopas under tillståndet connecting och tonas ut när agenten ansluter. Skicka true för den inbyggda standardringsignalen, eller en URL-sträng för att använda din egen ljudfil.
Med händelseåteranrop
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}
</>
)
}Helt anpassat användargränssnitt
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
Rendera alltid phone.audio
Elementet phone.audio är osynligt men krävs. Placera det var som helst i din JSX -- det renderar ingen synlig DOM men hanterar WebRTC-ljudanslutningen internt.
Inaktivera knappen under anslutning
Tillståndet connecting kan vara i 1–3 sekunder. Inaktivera samtalsknappen under detta tillstånd för att förhindra dubbla anslutningsförsök.
Hantera feltillståndet smidigt
När tillståndet är error visar du phone.error för användaren och låter samtalsknappen vara aktiverad. Hooken lämnar inte tillståndet error på egen hand -- om du anropar connect() igen startar ett nytt försök och det tidigare felet rensas.
Använd callbacks för bieffekter
Callbackarna onConnect, onDisconnect och onError är idealiska för analys, loggning eller för att utlösa annan programlogik utan att polla tillståndet.
Läs ljudnivåer från audioLevelRef
audioLevelRef är den enda källan för ljudnivåer i realtid. Läs audioLevelRef.current inuti requestAnimationFrame för mjuka animationer som vågformer (att läsa en ref orsakar inga omrenderingar), eller sampla den med ett intervall och lagra resultatet i state för React-renderat gränssnitt. Talet audioLevel är föråldrat och alltid 0 -- bygg inte logik på det.