Headless Hook
Хукът useThunderPhone ви дава пълен контрол върху потребителския интерфейс, докато ThunderPhone управлява гласовата сесия, маршрутизирането на аудиото и състоянието на връзката. Използвайте го, когато искате изцяло персонализиран интерфейс — със собствени бутони, оформления, анимации и брандиране — докато ThunderPhone се грижи за всичко във фонов режим.
Кога да използвате хука без интерфейс
Предварително създаденият компонент ThunderPhoneWidget покрива повечето случаи на употреба, но използвайте хука без интерфейс, когато се нуждаете от:
- Напълно персонализиран интерфейс за обаждания, който съответства на дизайнерската система на приложението ви
- Визуализации, реагиращи на аудио (вълнови форми, сфери, пулсиращи индикатори), управлявани от аудио нива в реално време
- Персонализирани потоци за обаждания, като формуляри преди обаждане, анкети след обаждане или вграден чат наред с гласовата комуникация
- Интеграция в съществуваща библиотека от компоненти (Material UI, Chakra, Radix и т.н.)
Инсталиране
npm install @thunderphone/widget
Основна употреба
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}
</>
)
}
Опции
Подайте тези опции към useThunderPhone чрез UseThunderPhoneOptions:
| Опция | Тип | Задължителна | По подразбиране | Описание |
|---|---|---|---|---|
publishableKey | string | Да | -- | Публичен API ключ (pk_live_...). Агентът се определя автоматично от конфигурацията на компонента за ключа. |
apiBase | string | Не | 'https://api.thunderphone.com/v1' | Заместващ основен URL адрес на API. |
language | string | Не | -- | Заместващ език за сесията — езиков код или локализация, като en, es или fr-FR. Ако не е зададен, се прилага конфигурираният език на агента. |
voice | string | Не | -- | Заместващ глас за сесията — име на глас, като maria. Ако не е зададен, се прилага конфигурираният глас на агента. |
context | string | Не | -- | Фактически контекст за страница или сайт за сесията, подаван към агента. Отрязва се от сървъра до 12 000 знака. |
onConnect | () => void | Не | -- | Извиква се при свързване на гласовата сесия. |
onDisconnect | () => void | Не | -- | Извиква се при приключване на сесията. |
onError | (error) => void | Не | -- | Извиква се при грешки. Грешката има полета error (код) и message. |
ringtone | boolean | string | Не | false | Възпроизвежда мелодия за позвъняване при свързване. true за мелодията по подразбиране или URL низ за персонализирано аудио. |
Връщана стойност
Хукът връща обект UseThunderPhoneReturn:
| Свойство | Тип | Описание |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Текущо състояние на връзката. |
connect | () => void | Стартирайте гласова сесия. |
disconnect | () => void | Прекратете текущата сесия. |
toggleMute | () => void | Включете или изключете заглушаването на микрофона. |
isMuted | boolean | Дали микрофонът в момента е заглушен. |
error | string | undefined | Съобщение за грешка, когато състоянието е 'error'. |
agentName | string | undefined | Показвано име на свързания агент. |
audioLevel | number | Отпада -- винаги 0. Статичен заместител, запазен за обратна съвместимост; никога не се актуализира. Вместо това четете audioLevelRef.current. |
audioLevelRef | React.RefObject<number> | Променяема референция, съдържаща аудионивото в реално време (0--1) -- по-силното от гласа на агента и микрофона на посетителя -- актуализирано при всеки кадър на анимацията, извън цикъла за рендиране на React. Четете audioLevelRef.current в цикли requestAnimationFrame за плавни анимации без накъсване или го вземайте на интервали, когато ви е необходима стойността в състоянието на React. |
audio | ReactNode | Невидим елемент, който обработва аудиовръзката -- трябва да бъде рендиран. |
Аудиореактивен интерфейс
Референцията audioLevelRef ви предоставя аудионива с честота на кадрите, без да задейства повторно рендиране на React, което я прави идеална за плавни визуализации на вълнови форми, пулсиращи сфери или всяка анимация, свързана с разговора. Нивото отразява по-силния звук: гласа на агента или микрофона на посетителя.
Пример за вълнова форма
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>
)
}
Пример за пулсираща сфера
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>
)
}
Пример за индикатор за говорене
За интерфейс, рендиран от React, който се променя според силата на звука — например значка „говори“, базирана на праг — отчитайте audioLevelRef.current през интервал и съхранявайте резултата в състоянието:
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>
)
}
Машина на състоянията
Свойството state следва този жизнен цикъл:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Състояние | Описание |
|---|---|
idle | Няма активна сесия. Готово е за извикване на connect(). |
connecting | Сесията се установява. Деактивирайте бутона за обаждане през това състояние. |
connected | Гласовата сесия е активна. Потребителят разговаря с агента. |
disconnected | Сесията е приключила коректно. Автоматично преминава обратно към idle след 1,5 секунди. |
error | Нещо се обърка. Проверете phone.error за съобщението. Състоянието не се изчиства само — повторното извикване на connect() започва нов опит и нулира грешката. |
Примери
С управление на заглушаването
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>
)
}
С мелодия за звънене
Възпроизвеждайте звук на звънене по време на свързване, за да симулирате телефонно обаждане:
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}
</>
)
}
Мелодията за звънене се повтаря по време на състоянието connecting и заглъхва, когато агентът се свърже. Подайте true, за да използвате вградената мелодия по подразбиране, или URL низ, за да използвате собствен аудиофайл.
С обратно извикване на събития
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}
</>
)
}
Напълно персонализиран потребителски интерфейс
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>
)
}
Съвети
Винаги рендирайте phone.audio
Елементът phone.audio е невидим, но задължителен. Поставете го където и да е във вашия JSX -- той не рендира видим DOM, но управлява вътрешно WebRTC аудио връзката.
Деактивирайте бутона при свързване
Състоянието connecting може да продължи 1–3 секунди. Деактивирайте бутона за обаждане през това състояние, за да предотвратите дублирани опити за свързване.
Обработвайте коректно състоянието за грешка
Когато състоянието е error, покажете phone.error на потребителя и оставете бутона за обаждане активен. Hook-ът не напуска самостоятелно състоянието error -- повторното извикване на connect() започва нов опит и изчиства предишната грешка.
Използвайте callback функции за странични ефекти
Callback функциите onConnect, onDisconnect и onError са подходящи за анализи, регистриране или задействане на друга логика на приложението без периодична проверка на състоянието.
Четете нивата на аудиото от audioLevelRef
audioLevelRef е единственият източник на аудио ниво в реално време. Четете audioLevelRef.current в requestAnimationFrame за плавни анимации, като вълнови форми (четенето на ref не предизвиква повторно рендиране), или го проверявайте през интервал и съхранявайте резултата в state за интерфейс, рендиран от React. Числото audioLevel е отхвърлено и винаги е 0 -- не изграждайте логика върху него.