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() покреће нови покушај и брише претходну грешку.
Користите повратне функције за споредне ефекте
Повратне функције onConnect, onDisconnect и onError идеалне су за аналитику, евидентирање или покретање друге логике апликације без провере стања у интервалима.
Читајте нивое звука из audioLevelRef
audioLevelRef је једини извор нивоа звука уживо. Читајте audioLevelRef.current унутар requestAnimationFrame за глатке анимације као што су таласни облици (читање референце не изазива поновно рендеровање) или га узоркујте у интервалима и сачувајте резултат у стању за UI који рендерује React. Број audioLevel је застарео и увек је 0 -- немојте градити логику на њему.