Hook headless
Creați o interfață vocală complet personalizată cu hook-ul React useThunderPhone
Hook-ul useThunderPhone vă oferă control complet asupra interfeței cu utilizatorul, în timp ce ThunderPhone gestionează sesiunea vocală, rutarea audio și starea conexiunii. Utilizați-l când doriți o interfață complet personalizată -- propriile butoane, aspecte, animații și elemente de branding -- în timp ce ThunderPhone gestionează totul în fundal.
Când să utilizați hook-ul Headless
Componenta predefinită ThunderPhoneWidget acoperă majoritatea cazurilor de utilizare, însă utilizați hook-ul headless când aveți nevoie de:
- O interfață de apel complet personalizată, care se potrivește cu sistemul de design al aplicației dumneavoastră
- Vizualizări reactive la audio (forme de undă, sfere, indicatori pulsați) bazate pe nivelurile audio în timp real
- Fluxuri de apel personalizate, precum formulare înainte de apel, sondaje după apel sau chat integrat alături de voce
- Integrare într-o bibliotecă de componente existentă (Material UI, Chakra, Radix etc.)
Instalare
npm install @thunderphone/widgetUtilizare de bază
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}
</>
)
}Opțiuni
Transmiteți aceste opțiuni către useThunderPhone prin UseThunderPhoneOptions:
| Opțiune | Tip | Obligatoriu | Implicit | Descriere |
|---|---|---|---|---|
publishableKey | string | Da | -- | Cheie API publicabilă (pk_live_...). Agentul este determinat automat din configurația widget-ului cheii. |
apiBase | string | Nu | 'https://api.thunderphone.com/v1' | Suprascrierea URL-ului de bază al API-ului. |
language | string | Nu | -- | Suprascrierea limbii pentru fiecare sesiune -- un cod de limbă sau o configurație regională, precum en, es sau fr-FR. Dacă nu este setată, se aplică limba configurată a agentului. |
voice | string | Nu | -- | Suprascrierea vocii pentru fiecare sesiune -- un nume de voce, precum maria. Dacă nu este setată, se aplică vocea configurată a agentului. |
context | string | Nu | -- | Context factual al paginii sau site-ului pentru fiecare sesiune, transmis agentului. Este trunchiat pe server la 12.000 de caractere. |
onConnect | () => void | Nu | -- | Apelată când se conectează sesiunea vocală. |
onDisconnect | () => void | Nu | -- | Apelată când se încheie sesiunea. |
onError | (error) => void | Nu | -- | Apelată la erori. Eroarea are câmpurile error (cod) și message. |
ringtone | boolean | string | Nu | false | Redați un ton de apel în timpul conectării. true pentru tonul de apel implicit sau un șir URL pentru audio personalizat. |
Valoare returnată
Hook-ul returnează un obiect UseThunderPhoneReturn:
| Proprietate | Tip | Descriere |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Starea curentă a conexiunii. |
connect | () => void | Porniți o sesiune vocală. |
disconnect | () => void | Încheiați sesiunea curentă. |
toggleMute | () => void | Activați/dezactivați dezactivarea sunetului pentru microfon. |
isMuted | boolean | Indică dacă microfonul este dezactivat în prezent. |
error | string | undefined | Mesaj de eroare când starea este 'error'. |
agentName | string | undefined | Numele afișat al agentului conectat. |
audioLevel | number | Depreciat -- întotdeauna 0. Un substituent static păstrat pentru compatibilitate cu versiunile anterioare; nu se actualizează niciodată. Citiți în schimb audioLevelRef.current. |
audioLevelRef | React.RefObject<number> | O referință mutabilă care conține nivelul audio în timp real (0--1) -- cel mai ridicat dintre vocea agentului și microfonul vizitatorului -- actualizată la fiecare cadru de animație, în afara ciclului de randare React. Citiți audioLevelRef.current în buclele requestAnimationFrame pentru animații fluide, fără sacadări, sau eșantionați-l la un interval atunci când aveți nevoie de valoare în starea React. |
audio | ReactNode | Element invizibil care gestionează conexiunea audio -- trebuie randat. |
Interfață reactivă la audio
Referința audioLevelRef vă oferă niveluri audio la rata cadrelor fără a declanșa re-randări React, ceea ce o face ideală pentru vizualizări fluide ale formei de undă, sfere pulsante sau orice animație legată de conversație. Nivelul reflectă sursa mai puternică: vocea agentului sau microfonul vizitatorului.
Exemplu de formă de undă
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>
)
}Exemplu de sferă pulsantă
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>
)
}Exemplu de indicator pentru vorbire
Pentru o interfață redată de React care se modifică în funcție de volum -- precum o insignă „vorbește” bazată pe prag -- eșantionați audioLevelRef.current la un interval și stocați rezultatul în stare:
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>
)
}Mașina de stări
Proprietatea state urmează acest ciclu de viață:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Stare | Descriere |
|---|---|
idle | Nicio sesiune activă. Gata pentru apelarea connect(). |
connecting | Sesiunea este în curs de stabilire. Dezactivați butonul de apel în această stare. |
connected | Sesiunea vocală este activă. Utilizatorul vorbește cu agentul. |
disconnected | Sesiunea s-a încheiat corect. Revine automat la idle după 1,5 secunde. |
error | Ceva nu a funcționat. Verificați phone.error pentru mesaj. Starea nu se șterge automat -- apelarea din nou a connect() începe o nouă încercare și resetează eroarea. |
Exemple
Cu controlul dezactivării sunetului
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>
)
}Cu ton de apel
Redați un sunet de apel în timpul conectării pentru a simula un apel telefonic:
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}
</>
)
}Tonul de apel se redă în buclă în starea connecting și se estompează când agentul se conectează. Transmiteți true pentru tonul de apel implicit integrat sau un șir URL pentru a utiliza propriul fișier audio.
Cu callback-uri de evenimente
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}
</>
)
}Interfață UI complet personalizată
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>
)
}Sfaturi
Redați întotdeauna phone.audio
Elementul phone.audio este invizibil, dar obligatoriu. Plasați-l oriunde în JSX — nu redă niciun DOM vizibil, dar gestionează intern conexiunea audio WebRTC.
Dezactivați butonul în timpul conectării
Starea connecting poate dura 1-3 secunde. Dezactivați butonul de apel în această stare pentru a preveni încercările de conectare duplicate.
Gestionați elegant starea de eroare
Când starea este error, afișați utilizatorului phone.error și păstrați activat butonul de apel. Hook-ul nu părăsește singur starea error — apelarea din nou a connect() începe o încercare nouă și șterge eroarea anterioară.
Utilizați callback-uri pentru efecte secundare
Callback-urile onConnect, onDisconnect și onError sunt ideale pentru analiză, jurnalizare sau declanșarea altor logici ale aplicației fără interogarea repetată a stării.
Citiți nivelurile audio din audioLevelRef
audioLevelRef este singura sursă live pentru nivelul audio. Citiți audioLevelRef.current în interiorul requestAnimationFrame pentru animații fluide, cum ar fi formele de undă (citirea unui ref nu provoacă rerandări), sau eșantionați-l la un interval și stocați rezultatul în stare pentru interfața redată de React. Numărul audioLevel este depreciat și este întotdeauna 0 — nu construiți logică bazată pe acesta.