ThunderPhone 2.0 kini resmi hadir.Layanan mandiri, mulai dari 2¢/menit.Baca pengumumannya

Widget

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/widget

Penggunaan 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:

OpsiTipeWajibDefaultDeskripsi
publishableKeystringYa--Kunci API publik (pk_live_...). Agen ditentukan secara otomatis dari konfigurasi widget kunci tersebut.
apiBasestringTidak'https://api.thunderphone.com/v1'Penggantian URL dasar API.
languagestringTidak--Penggantian bahasa per sesi -- kode bahasa atau lokal seperti en, es, atau fr-FR. Jika tidak ditetapkan, bahasa yang dikonfigurasi untuk agen akan digunakan.
voicestringTidak--Penggantian suara per sesi -- nama suara seperti maria. Jika tidak ditetapkan, suara yang dikonfigurasi untuk agen akan digunakan.
contextstringTidak--Konteks faktual halaman atau situs per sesi yang diteruskan ke agen. Dipotong di sisi server hingga 12.000 karakter.
onConnect() => voidTidak--Dipanggil saat sesi suara terhubung.
onDisconnect() => voidTidak--Dipanggil saat sesi berakhir.
onError(error) => voidTidak--Dipanggil saat terjadi kesalahan. Error memiliki field error (kode) dan message.
ringtoneboolean | stringTidakfalsePutar nada dering saat menghubungkan. true untuk nada dering default, atau string URL untuk audio kustom.

Nilai Kembalian

Hook mengembalikan objek UseThunderPhoneReturn:

PropertiTipeDeskripsi
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Status koneksi saat ini.
connect() => voidMemulai sesi suara.
disconnect() => voidMengakhiri sesi saat ini.
toggleMute() => voidMengaktifkan/menonaktifkan senyap mikrofon.
isMutedbooleanApakah mikrofon saat ini disenyapkan.
errorstring | undefinedPesan kesalahan saat status adalah 'error'.
agentNamestring | undefinedNama tampilan agen yang terhubung.
audioLevelnumberTidak digunakan lagi -- selalu 0. Placeholder statis yang dipertahankan untuk kompatibilitas mundur; nilainya tidak pernah diperbarui. Gunakan audioLevelRef.current sebagai gantinya.
audioLevelRefReact.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.
audioReactNodeElemen 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)
StatusDeskripsi
idleTidak ada sesi aktif. Siap memanggil connect().
connectingSesi sedang dibuat. Nonaktifkan tombol panggilan selama status ini.
connectedSesi suara aktif. Pengguna sedang berbicara dengan agen.
disconnectedSesi telah berakhir dengan normal. Beralih kembali ke idle secara otomatis setelah 1,5 detik.
errorTerjadi 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.