Headless Hook
Vytvořte plně vlastní hlasové uživatelské rozhraní pomocí React hooku useThunderPhone
Hook useThunderPhone vám poskytuje úplnou kontrolu nad uživatelským rozhraním, zatímco ThunderPhone spravuje hlasovou relaci, směrování zvuku a stav připojení. Použijte ho, když chcete plně vlastní uživatelské rozhraní -- vlastní tlačítka, rozvržení, animace a branding -- zatímco ThunderPhone se postará o vše na pozadí.
Kdy použít headless hook
Předpřipravená komponenta ThunderPhoneWidget pokrývá většinu případů použití, ale headless hook použijte, když potřebujete:
- Zcela vlastní uživatelské rozhraní hovoru, které odpovídá designovému systému vaší aplikace
- Vizualizace reagující na zvuk (křivky zvuku, koule, pulzující indikátory) řízené úrovněmi zvuku v reálném čase
- Vlastní toky hovoru, jako jsou formuláře před hovorem, dotazníky po hovoru nebo integrovaný chat vedle hlasové komunikace
- Integraci do existující knihovny komponent (Material UI, Chakra, Radix atd.)
Instalace
npm install @thunderphone/widgetZákladní použití
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}
</>
)
}Možnosti
Tyto možnosti předejte do useThunderPhone prostřednictvím UseThunderPhoneOptions:
| Možnost | Typ | Povinné | Výchozí | Popis |
|---|---|---|---|---|
publishableKey | string | Ano | -- | Publikovatelný klíč API (pk_live_...). Agent se automaticky určí z konfigurace widgetu daného klíče. |
apiBase | string | Ne | 'https://api.thunderphone.com/v1' | Přepsání základní adresy URL API. |
language | string | Ne | -- | Přepsání jazyka pro relaci -- kód jazyka nebo národní prostředí, například en, es nebo fr-FR. Pokud není nastaveno, použije se nakonfigurovaný jazyk agenta. |
voice | string | Ne | -- | Přepsání hlasu pro relaci -- název hlasu, například maria. Pokud není nastaveno, použije se nakonfigurovaný hlas agenta. |
context | string | Ne | -- | Faktický kontext stránky nebo webu pro relaci předaný agentovi. Na straně serveru zkrácený na 12 000 znaků. |
onConnect | () => void | Ne | -- | Volá se při připojení hlasové relace. |
onDisconnect | () => void | Ne | -- | Volá se při ukončení relace. |
onError | (error) => void | Ne | -- | Volá se při chybách. Chyba obsahuje pole error (kód) a message. |
ringtone | boolean | string | Ne | false | Přehrává vyzvánění během připojování. true pro výchozí vyzvánění nebo řetězec URL pro vlastní zvuk. |
Návratová hodnota
Hook vrací objekt UseThunderPhoneReturn:
| Vlastnost | Typ | Popis |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Aktuální stav připojení. |
connect | () => void | Spustí hlasovou relaci. |
disconnect | () => void | Ukončí aktuální relaci. |
toggleMute | () => void | Zapne nebo vypne ztlumení mikrofonu. |
isMuted | boolean | Zda je mikrofon aktuálně ztlumený. |
error | string | undefined | Chybová zpráva, když je stav 'error'. |
agentName | string | undefined | Zobrazovaný název připojeného agenta. |
audioLevel | number | Zastaralé -- vždy 0. Statický zástupný symbol zachovaný kvůli zpětné kompatibilitě; nikdy se neaktualizuje. Místo toho čtěte audioLevelRef.current. |
audioLevelRef | React.RefObject<number> | Měnitelná reference obsahující úroveň zvuku v reálném čase (0--1) -- vyšší z hlasitosti hlasu agenta a mikrofonu návštěvníka -- aktualizovaná v každém animačním snímku mimo cyklus vykreslování Reactu. Pro plynulé animace bez zasekávání čtěte audioLevelRef.current uvnitř smyček requestAnimationFrame, nebo hodnotu vzorkujte v intervalu, když ji potřebujete ve stavu Reactu. |
audio | ReactNode | Neviditelný prvek, který zajišťuje zvukové připojení -- musí být vykreslen. |
Uživatelské rozhraní reagující na zvuk
Ref audioLevelRef poskytuje úrovně zvuku ve snímkové frekvenci bez vyvolání opětovného vykreslení Reactu, takže je ideální pro plynulé vizualizace zvukové vlny, pulzující koule nebo jakoukoli animaci navázanou na konverzaci. Úroveň odráží hlasitější ze dvou zdrojů: hlas agenta nebo mikrofon návštěvníka.
Příklad zvukové vlny
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>
)
}Příklad pulzující koule
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>
)
}Příklad indikátoru mluvení
Pro uživatelské rozhraní vykreslované Reactem, které se mění podle hlasitosti – například štítek „mluví“ založený na prahové hodnotě – načítejte audioLevelRef.current v intervalu a ukládejte výsledek do stavu:
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>
)
}Stavový automat
Vlastnost state prochází tímto životním cyklem:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Stav | Popis |
|---|---|
idle | Žádná aktivní relace. Připraveno k volání connect(). |
connecting | Relace se navazuje. V tomto stavu zakažte tlačítko volání. |
connected | Hlasová relace je aktivní. Uživatel mluví s agentem. |
disconnected | Relace byla řádně ukončena. Po 1,5 sekundě se automaticky přepne zpět do stavu idle. |
error | Něco se pokazilo. Zkontrolujte zprávu v phone.error. Stav se sám nevymaže -- opětovné volání connect() zahájí nový pokus a resetuje chybu. |
Příklady
S ovládáním ztlumení
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 vyzváněním
Při připojování přehrajte vyzváněcí zvuk pro simulaci telefonního hovoru:
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}
</>
)
}Vyzvánění se během stavu connecting opakuje a po připojení agenta postupně utichne. Pro vestavěné výchozí vyzvánění předejte hodnotu true, nebo řetězec URL pro použití vlastního zvukového souboru.
S obslužnými funkcemi událostí
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}
</>
)
}Plně vlastní uživatelské rozhraní
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>
)
}Tipy
Vždy vykreslujte phone.audio
Prvek phone.audio je neviditelný, ale povinný. Umístěte jej kamkoli do JSX -- nevykresluje žádný viditelný DOM, ale interně spravuje zvukové připojení WebRTC.
Během připojování tlačítko deaktivujte
Stav connecting může trvat 1–3 sekundy. Během tohoto stavu deaktivujte tlačítko volání, abyste zabránili duplicitním pokusům o připojení.
Stav chyby zpracujte vhodně
Když je stav error, zobrazte uživateli phone.error a tlačítko volání ponechte aktivní. Hook stav error sám neopustí -- opětovné volání connect() zahájí nový pokus a vymaže předchozí chybu.
Pro vedlejší efekty používejte callbacky
Callbacky onConnect, onDisconnect a onError jsou ideální pro analytiku, protokolování nebo spuštění jiné logiky aplikace bez dotazování na stav.
Úrovně zvuku čtěte z audioLevelRef
audioLevelRef je jediný živý zdroj úrovně zvuku. Pro plynulé animace, například průběhy vln, čtěte audioLevelRef.current uvnitř requestAnimationFrame (čtení ref nezpůsobuje opětovné vykreslení), nebo jej vzorkujte v intervalu a výsledek ukládejte do stavu pro uživatelské rozhraní vykreslované Reactem. Číslo audioLevel je zastaralé a vždy má hodnotu 0 -- nestavte na něm logiku.