Headless Hook
Hozzon létre teljesen egyedi hangalapú felhasználói felületet a useThunderPhone React hookkal
A useThunderPhone hook teljes körű irányítást biztosít a felhasználói felület felett, miközben a ThunderPhone kezeli a hangmunkamenetet, a hangirányítást és a kapcsolat állapotát. Akkor használja, ha teljesen egyedi felhasználói felületre van szüksége -- saját gombokkal, elrendezésekkel, animációkkal és arculattal --, miközben a ThunderPhone a háttérben mindent kezel.
Mikor használja a Headless hookot
Az előre elkészített ThunderPhoneWidget komponens a legtöbb használati esetet lefedi, de használja a headless hookot, ha a következőkre van szüksége:
- Teljesen egyedi hívási felületre, amely illeszkedik az alkalmazása tervezési rendszeréhez
- Valós idejű hangszintek által vezérelt, hangra reagáló vizualizációkra (hullámformák, gömbök, pulzáló jelzők)
- Egyedi hívási folyamatokra, például hívás előtti űrlapokra, hívás utáni felmérésekre vagy a hang melletti beágyazott chatre
- Integrációra meglévő komponenskönyvtárba (Material UI, Chakra, Radix stb.)
Telepítés
npm install @thunderphone/widgetAlapszintű használat
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}
</>
)
}Beállítások
Ezeket a beállításokat adja át a useThunderPhone hooknak a UseThunderPhoneOptions használatával:
| Beállítás | Típus | Kötelező | Alapértelmezett | Leírás |
|---|---|---|---|---|
publishableKey | string | Igen | -- | Publikálható API-kulcs (pk_live_...). Az AI-ügynököt a kulcs widget-konfigurációja alapján automatikusan határozza meg. |
apiBase | string | Nem | 'https://api.thunderphone.com/v1' | API-alap URL felülírása. |
language | string | Nem | -- | Munkamenetenkénti nyelvi felülírás -- nyelvkód vagy területi beállítás, például en, es vagy fr-FR. Ha nincs beállítva, az AI-ügynök konfigurált nyelve érvényes. |
voice | string | Nem | -- | Munkamenetenkénti hangfelülírás -- hangnév, például maria. Ha nincs beállítva, az AI-ügynök konfigurált hangja érvényes. |
context | string | Nem | -- | Az AI-ügynöknek átadott, munkamenetenkénti tényszerű oldal- vagy webhelykontextus. A szerveroldalon 12 000 karakterre csonkolva. |
onConnect | () => void | Nem | -- | A hangmunkamenet kapcsolódásakor hívódik meg. |
onDisconnect | () => void | Nem | -- | A munkamenet befejeződésekor hívódik meg. |
onError | (error) => void | Nem | -- | Hiba esetén hívódik meg. A hiba error (kód) és message mezőket tartalmaz. |
ringtone | boolean | string | Nem | false | Csengetési hang lejátszása kapcsolódás közben. Az alapértelmezett csengetési hanghoz true, egyéni hanghoz pedig URL-karakterlánc használható. |
Visszatérési érték
A hook egy UseThunderPhoneReturn objektumot ad vissza:
| Tulajdonság | Típus | Leírás |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Az aktuális kapcsolati állapot. |
connect | () => void | Hangalapú munkamenet indítása. |
disconnect | () => void | Az aktuális munkamenet befejezése. |
toggleMute | () => void | A mikrofon némításának be- vagy kikapcsolása. |
isMuted | boolean | A mikrofon jelenleg némítva van-e. |
error | string | undefined | Hibaüzenet, amikor az állapot 'error'. |
agentName | string | undefined | A csatlakoztatott ügynök megjelenített neve. |
audioLevel | number | Elavult -- mindig 0. Visszafelé kompatibilitás miatt megtartott statikus helyőrző; soha nem frissül. Ehelyett olvassa az audioLevelRef.current értékét. |
audioLevelRef | React.RefObject<number> | Egy módosítható ref, amely a valós idejű hangszintet (0--1) tartalmazza -- az ügynök hangja és a látogató mikrofonja közül a hangosabbat --, és minden animációs képkockán frissül, a React renderelési ciklusán kívül. A zökkenőmentes, akadásmentes animációkhoz olvassa az audioLevelRef.current értékét a requestAnimationFrame ciklusokon belül, vagy mintavételezze időközönként, amikor az értékre React-állapotban van szüksége. |
audio | ReactNode | Láthatatlan elem, amely kezeli a hangkapcsolatot -- renderelni kell. |
Hangra reagáló felhasználói felület
Az audioLevelRef ref képkockasebességű hangszinteket biztosít React-újrarenderelés kiváltása nélkül, ezért ideális sima hullámforma-vizualizációk, pulzáló gömbök vagy bármely, a beszélgetéshez kötött animáció vezérlésére. A szint azt tükrözi, amelyik hangosabb: az ügynök hangja vagy a látogató mikrofonja.
Hullámforma-példa
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>
)
}Pulzáló gömb példa
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>
)
}Beszédjelző példa
A hangerővel változó, React által renderelt felhasználói felületekhez -- például egy küszöbérték-alapú „beszél” jelvényhez -- adott időközönként olvassa ki az audioLevelRef.current értékét, és tárolja az eredményt állapotban:
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>
)
}Állapotgép
A state tulajdonság az alábbi életciklust követi:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Állapot | Leírás |
|---|---|
idle | Nincs aktív munkamenet. Készen áll a connect() hívására. |
connecting | A munkamenet létrehozása folyamatban van. Ebben az állapotban tiltsa le a hívás gombot. |
connected | A hangalapú munkamenet aktív. A felhasználó az ügynökkel beszél. |
disconnected | A munkamenet szabályosan befejeződött. 1,5 másodperc után automatikusan visszavált idle állapotba. |
error | Hiba történt. Az üzenetért ellenőrizze a phone.error értékét. Az állapot nem törlődik magától -- a connect() ismételt hívása új próbálkozást indít, és visszaállítja a hibát. |
Példák
Némításvezérléssel
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>
)
}Csengőhanggal
Játsszon le csengőhangot a csatlakozás során telefonhívás szimulálásához:
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}
</>
)
}A csengőhang a connecting állapot alatt ismétlődik, és elhalkul, amikor az ügynök csatlakozik. A beépített alapértelmezett csengőhanghoz adjon át true értéket, saját hangfájl használatához pedig URL-karakterláncot.
Esemény-visszahívásokkal
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}
</>
)
}Teljesen egyedi felület
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>
)
}Tippek
A phone.audio mindig legyen renderelve
A phone.audio elem láthatatlan, de kötelező. Helyezze el bárhol a JSX-ben -- nem renderel látható DOM-ot, de belsőleg kezeli a WebRTC-hangkapcsolatot.
Kapcsolódás közben tiltsa le a gombot
A connecting állapot 1–3 másodpercig tarthat. Ebben az állapotban tiltsa le a hívásgombot, hogy megelőzze az ismétlődő kapcsolódási kísérleteket.
Kezelje elegánsan a hibaállapotot
Ha az állapot error, jelenítse meg a felhasználónak a phone.error értékét, és hagyja engedélyezve a hívásgombot. A hook nem lép ki magától az error állapotból -- a connect() újbóli meghívása új kísérletet indít, és törli az előző hibát.
Használjon visszahívásokat a mellékhatásokhoz
Az onConnect, onDisconnect és onError visszahívások ideálisak elemzésekhez, naplózáshoz vagy más alkalmazáslogika aktiválásához az állapot lekérdezése nélkül.
Olvassa az audio szinteket az audioLevelRef értékéből
Az audioLevelRef az egyetlen élő hangszintforrás. A hullámformákhoz hasonló folyamatos animációkhoz olvassa az audioLevelRef.current értékét a requestAnimationFrame használatával (egy ref olvasása nem okoz újrarenderelést), vagy mintavételezze időközönként, és tárolja az eredményt állapotban a React által renderelt felhasználói felülethez. Az audioLevel szám elavult, és mindig 0 -- ne építsen rá logikát.