Headless Hook
Hook useThunderPhone daje vam potpunu kontrolu nad korisničkim sučeljem, dok ThunderPhone upravlja glasovnom sesijom, usmjeravanjem zvuka i stanjem veze. Upotrijebite ga kada želite potpuno prilagođeno korisničko sučelje -- vlastite gumbe, rasporede, animacije i brendiranje -- dok ThunderPhone upravlja svime u pozadini.
Kada koristiti hook bez sučelja
Unaprijed izrađena komponenta ThunderPhoneWidget pokriva većinu slučajeva upotrebe, ali upotrijebite hook bez sučelja kada trebate:
- Potpuno prilagođeno korisničko sučelje za pozive koje odgovara sustavu dizajna vaše aplikacije
- Vizualizacije koje reagiraju na zvuk (valni oblici, kugle, pulsirajući indikatori) pokretane razinama zvuka u stvarnom vremenu
- Prilagođene tijekove poziva, poput obrazaca prije poziva, anketa nakon poziva ili ugrađenog chata uz glasovnu komunikaciju
- Integraciju u postojeću biblioteku komponenti (Material UI, Chakra, Radix itd.)
Instalacija
npm install @thunderphone/widget
Osnovna upotreba
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}
</>
)
}
Opcije
Proslijedite ove opcije u useThunderPhone putem UseThunderPhoneOptions:
| Opcija | Vrsta | Obavezno | Zadano | Opis |
|---|---|---|---|---|
publishableKey | string | Da | -- | Javni API ključ (pk_live_...). Glasovni agent automatski se određuje iz konfiguracije widgeta ključa. |
apiBase | string | Ne | 'https://api.thunderphone.com/v1' | Zamjena za osnovni URL API-ja. |
language | string | Ne | -- | Zamjena jezika po sesiji -- jezični kôd ili lokalizacija, poput en, es ili fr-FR. Ako nije postavljeno, primjenjuje se konfigurirani jezik agenta. |
voice | string | Ne | -- | Zamjena glasa po sesiji -- naziv glasa, poput maria. Ako nije postavljeno, primjenjuje se konfigurirani glas agenta. |
context | string | Ne | -- | Činjenični kontekst stranice ili web-mjesta po sesiji koji se prosljeđuje agentu. Na strani poslužitelja skraćuje se na 12.000 znakova. |
onConnect | () => void | Ne | -- | Poziva se kada se glasovna sesija poveže. |
onDisconnect | () => void | Ne | -- | Poziva se kada sesija završi. |
onError | (error) => void | Ne | -- | Poziva se pri pogreškama. Pogreška ima polja error (kôd) i message. |
ringtone | boolean | string | Ne | false | Reproducira melodiju zvona tijekom povezivanja. true za zadanu melodiju zvona ili URL niz za prilagođeni zvuk. |
Povratna vrijednost
Hook vraća objekt UseThunderPhoneReturn:
| Svojstvo | Vrsta | Opis |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Trenutačno stanje veze. |
connect | () => void | Pokrenite glasovnu sesiju. |
disconnect | () => void | Završite trenutačnu sesiju. |
toggleMute | () => void | Uključite/isključite utišavanje mikrofona. |
isMuted | boolean | Je li mikrofon trenutačno utišan. |
error | string | undefined | Poruka o pogrešci kada je stanje 'error'. |
agentName | string | undefined | Naziv za prikaz povezanog agenta. |
audioLevel | number | Zastarjelo -- uvijek 0. Statično rezervirano mjesto zadržano radi kompatibilnosti sa starijim verzijama; nikada se ne ažurira. Umjesto toga pročitajte audioLevelRef.current. |
audioLevelRef | React.RefObject<number> | Promjenjiva referenca koja sadrži razinu zvuka u stvarnom vremenu (0--1) -- glasniji od glasa agenta i mikrofona posjetitelja -- ažurirana u svakom okviru animacije, izvan Reactova ciklusa renderiranja. Za glatke animacije bez zastajkivanja pročitajte audioLevelRef.current unutar petlji requestAnimationFrame ili ga uzorkujte u intervalima kada vam je vrijednost potrebna u stanju Reacta. |
audio | ReactNode | Nevidljivi element koji upravlja audiovezom -- mora se renderirati. |
Korisničko sučelje koje reagira na zvuk
Ref audioLevelRef omogućuje vam razine zvuka pri brzini osvježavanja bez pokretanja ponovnog renderiranja Reacta, što ga čini idealnim za upravljanje glatkim vizualizacijama valnog oblika, pulsirajućim kuglama ili bilo kojom animacijom povezanom s razgovorom. Razina odražava glasniji izvor: glas agenta ili mikrofon posjetitelja.
Primjer valnog oblika
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>
)
}
Primjer pulsirajuće 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>
)
}
Primjer indikatora govora
Za korisničko sučelje koje renderira React i mijenja se s glasnoćom -- poput oznake „govori” koja se temelji na pragu -- uzorkujte audioLevelRef.current u intervalima i pohranite rezultat u stanje:
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>
)
}
Stanje automata
Svojstvo state prati ovaj životni ciklus:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Stanje | Opis |
|---|---|
idle | Nema aktivne sesije. Spremno za pozivanje connect(). |
connecting | Sesija se uspostavlja. Onemogućite gumb za poziv tijekom ovog stanja. |
connected | Glasovna sesija je aktivna. Korisnik razgovara s agentom. |
disconnected | Sesija je uredno završena. Automatski se vraća na idle nakon 1,5 sekundi. |
error | Nešto je pošlo po zlu. Poruku provjerite u phone.error. Stanje se ne briše samo od sebe -- ponovno pozivanje connect() pokreće novi pokušaj i poništava pogrešku. |
Primjeri
S kontrolom utišavanja
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>
)
}
S melodijom zvona
Reproducirajte zvuk zvonjave tijekom povezivanja kako biste simulirali telefonski poziv:
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}
</>
)
}
Melodija zvona ponavlja se tijekom stanja connecting i postupno se stišava kada se agent poveže. Proslijedite true za ugrađenu zadanu melodiju zvona ili URL niz za upotrebu vlastite audiodatoteke.
S povratnim pozivima događaja
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}
</>
)
}
Potpuno prilagođeno sučelje
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>
)
}
Savjeti
Uvijek renderirajte phone.audio
Element phone.audio nije vidljiv, ali je obavezan. Postavite ga bilo gdje u svoj JSX -- ne renderira vidljivi DOM, ali interno upravlja WebRTC audio vezom.
Onemogućite gumb tijekom povezivanja
Stanje connecting može trajati 1-3 sekunde. Onemogućite gumb za poziv tijekom tog stanja kako biste spriječili dvostruke pokušaje povezivanja.
Pravilno obradite stanje pogreške
Kada je stanje error, prikažite korisniku phone.error i zadržite omogućen gumb za poziv. Hook ne napušta stanje error samostalno -- ponovni poziv connect() pokreće novi pokušaj i briše prethodnu pogrešku.
Upotrebljavajte povratne funkcije za popratne učinke
Povratne funkcije onConnect, onDisconnect i onError idealne su za analitiku, zapisivanje ili pokretanje druge logike aplikacije bez anketiranja stanja.
Čitajte razine zvuka iz audioLevelRef
audioLevelRef jedini je aktivni izvor razine zvuka. Čitajte audioLevelRef.current unutar requestAnimationFrame za glatke animacije poput valnih oblika (čitanje refa ne uzrokuje ponovno renderiranje) ili ga uzorkujte u intervalima i pohranite rezultat u stanje za korisničko sučelje koje renderira React. Broj audioLevel zastario je i uvijek je 0 -- nemojte graditi logiku na njemu.