Headless Hook
Bangun UI suara yang sepenuhnya kustom dengan hook React useThunderPhone
Hook useThunderPhone memberi Anda kendali penuh atas antarmuka pengguna, sementara ThunderPhone mengelola sesi suara, perutean audio, dan status koneksi. Gunakan saat Anda menginginkan UI yang sepenuhnya kustom -- tombol, tata letak, animasi, dan branding Anda sendiri -- sementara ThunderPhone menangani semuanya di balik layar.
Kapan Menggunakan Hook Headless
Komponen ThunderPhoneWidget bawaan mencakup sebagian besar kasus penggunaan, tetapi gunakan hook headless saat Anda memerlukan:
- UI panggilan yang sepenuhnya kustom dan sesuai dengan sistem desain aplikasi Anda
- Visualisasi yang responsif terhadap audio (bentuk gelombang, orb, indikator berdenyut) yang didorong oleh level audio real-time
- Alur panggilan kustom seperti formulir sebelum panggilan, survei setelah panggilan, atau chat inline bersama suara
- Integrasi ke dalam library komponen yang sudah ada (Material UI, Chakra, Radix, dll.)
Instalasi
npm install @thunderphone/widgetPenggunaan Dasar
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}
</>
)
}Opsi
Teruskan opsi ini ke useThunderPhone melalui UseThunderPhoneOptions:
| Opsi | Tipe | Wajib | Default | Deskripsi |
|---|---|---|---|---|
publishableKey | string | Ya | -- | Kunci API publik (pk_live_...). Agen ditentukan secara otomatis dari konfigurasi widget kunci tersebut. |
apiBase | string | Tidak | 'https://api.thunderphone.com/v1' | Penggantian URL dasar API. |
language | string | Tidak | -- | Penggantian bahasa per sesi -- kode bahasa atau lokal seperti en, es, atau fr-FR. Jika tidak ditetapkan, bahasa yang dikonfigurasi untuk agen akan digunakan. |
voice | string | Tidak | -- | Penggantian suara per sesi -- nama suara seperti maria. Jika tidak ditetapkan, suara yang dikonfigurasi untuk agen akan digunakan. |
context | string | Tidak | -- | Konteks faktual halaman atau situs per sesi yang diteruskan ke agen. Dipotong di sisi server hingga 12.000 karakter. |
onConnect | () => void | Tidak | -- | Dipanggil saat sesi suara terhubung. |
onDisconnect | () => void | Tidak | -- | Dipanggil saat sesi berakhir. |
onError | (error) => void | Tidak | -- | Dipanggil saat terjadi kesalahan. Error memiliki field error (kode) dan message. |
ringtone | boolean | string | Tidak | false | Putar nada dering saat menghubungkan. true untuk nada dering default, atau string URL untuk audio kustom. |
Nilai Kembalian
Hook mengembalikan objek UseThunderPhoneReturn:
| Properti | Tipe | Deskripsi |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Status koneksi saat ini. |
connect | () => void | Memulai sesi suara. |
disconnect | () => void | Mengakhiri sesi saat ini. |
toggleMute | () => void | Mengaktifkan/menonaktifkan senyap mikrofon. |
isMuted | boolean | Apakah mikrofon saat ini disenyapkan. |
error | string | undefined | Pesan kesalahan saat status adalah 'error'. |
agentName | string | undefined | Nama tampilan agen yang terhubung. |
audioLevel | number | Tidak digunakan lagi -- selalu 0. Placeholder statis yang dipertahankan untuk kompatibilitas mundur; nilainya tidak pernah diperbarui. Gunakan audioLevelRef.current sebagai gantinya. |
audioLevelRef | React.RefObject<number> | Ref yang dapat diubah dan berisi level audio waktu nyata (0--1) -- yang lebih keras antara suara agen dan mikrofon pengunjung -- diperbarui pada setiap frame animasi, di luar siklus render React. Baca audioLevelRef.current di dalam loop requestAnimationFrame untuk animasi yang mulus tanpa tersendat, atau ambil sampelnya pada interval saat Anda memerlukan nilainya dalam state React. |
audio | ReactNode | Elemen tak terlihat yang menangani koneksi audio -- harus dirender. |
UI Reaktif Audio
Ref audioLevelRef memberi Anda level audio pada laju frame tanpa memicu render ulang React, sehingga ideal untuk menggerakkan visualisasi bentuk gelombang yang mulus, orb berdenyut, atau animasi apa pun yang terkait dengan percakapan. Level mencerminkan sumber yang lebih keras: suara agen atau mikrofon pengunjung.
Contoh Bentuk Gelombang
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>
)
}Contoh Orb Berdenyut
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>
)
}Contoh Indikator Berbicara
Untuk UI yang dirender React dan berubah sesuai volume -- seperti lencana "berbicara" berbasis ambang batas -- ambil sampel audioLevelRef.current pada interval tertentu dan simpan hasilnya dalam state:
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>
)
}Mesin Status
Properti state mengikuti siklus hidup ini:
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
\
--> error (stays until connect() is called again)
| Status | Deskripsi |
|---|---|
idle | Tidak ada sesi aktif. Siap memanggil connect(). |
connecting | Sesi sedang dibuat. Nonaktifkan tombol panggilan selama status ini. |
connected | Sesi suara aktif. Pengguna sedang berbicara dengan agen. |
disconnected | Sesi telah berakhir dengan normal. Beralih kembali ke idle secara otomatis setelah 1,5 detik. |
error | Terjadi kesalahan. Periksa phone.error untuk pesannya. Status tidak akan dihapus dengan sendirinya -- memanggil connect() lagi akan memulai upaya baru dan mereset kesalahan. |
Contoh
Dengan Kontrol Bisukan
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>
)
}Dengan Nada Dering
Putar suara dering saat menghubungkan untuk menyimulasikan panggilan telepon:
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}
</>
)
}Nada dering berulang selama status connecting dan memudar saat agen terhubung. Berikan true untuk nada dering bawaan, atau string URL untuk menggunakan file audio Anda sendiri.
Dengan Callback Peristiwa
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}
</>
)
}UI Kustom Penuh
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>
)
}Tips
Selalu render phone.audio
Elemen phone.audio tidak terlihat tetapi diperlukan. Tempatkan di mana saja dalam JSX Anda -- elemen ini tidak merender DOM yang terlihat, tetapi mengelola koneksi audio WebRTC secara internal.
Nonaktifkan tombol saat menghubungkan
Status connecting dapat berlangsung selama 1-3 detik. Nonaktifkan tombol panggilan selama status ini untuk mencegah upaya koneksi duplikat.
Tangani status error dengan baik
Saat statusnya error, tampilkan phone.error kepada pengguna dan biarkan tombol panggilan Anda tetap aktif. Hook tidak keluar dari status error dengan sendirinya -- memanggil connect() lagi memulai upaya baru dan menghapus error sebelumnya.
Gunakan callback untuk efek samping
Callback onConnect, onDisconnect, dan onError ideal untuk analitik, logging, atau memicu logika aplikasi lain tanpa melakukan polling pada status.
Baca level audio dari audioLevelRef
audioLevelRef adalah satu-satunya sumber level audio langsung. Baca audioLevelRef.current di dalam requestAnimationFrame untuk animasi mulus seperti bentuk gelombang (membaca ref tidak menyebabkan render ulang), atau ambil sampelnya pada interval tertentu dan simpan hasilnya dalam status untuk UI yang dirender React. Angka audioLevel sudah tidak digunakan lagi dan selalu 0 -- jangan membangun logika berdasarkan angka tersebut.