Headless Hook

Hook useThunderPhone daje vam potpunu kontrolu nad korisničkim sučeljem, dok ThunderPhone upravlja glasovnom sesijom, usmjeravanjem zvuka i stanjem veze. Upotrijebite ga kada želite potpuno prilagođeno korisničko sučelje -- vlastite gumbe, rasporede, animacije i brendiranje -- dok ThunderPhone upravlja svime u pozadini.

Kada koristiti hook bez sučelja

Unaprijed izrađena komponenta ThunderPhoneWidget pokriva većinu slučajeva upotrebe, ali upotrijebite hook bez sučelja kada trebate:


Instalacija

npm install @thunderphone/widget

Osnovna upotreba

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}
    </>
  )
}

Opcije

Proslijedite ove opcije u useThunderPhone putem UseThunderPhoneOptions:

OpcijaVrstaObaveznoZadanoOpis
publishableKeystringDa--Javni API ključ (pk_live_...). Glasovni agent automatski se određuje iz konfiguracije widgeta ključa.
apiBasestringNe'https://api.thunderphone.com/v1'Zamjena za osnovni URL API-ja.
languagestringNe--Zamjena jezika po sesiji -- jezični kôd ili lokalizacija, poput en, es ili fr-FR. Ako nije postavljeno, primjenjuje se konfigurirani jezik agenta.
voicestringNe--Zamjena glasa po sesiji -- naziv glasa, poput maria. Ako nije postavljeno, primjenjuje se konfigurirani glas agenta.
contextstringNe--Činjenični kontekst stranice ili web-mjesta po sesiji koji se prosljeđuje agentu. Na strani poslužitelja skraćuje se na 12.000 znakova.
onConnect() => voidNe--Poziva se kada se glasovna sesija poveže.
onDisconnect() => voidNe--Poziva se kada sesija završi.
onError(error) => voidNe--Poziva se pri pogreškama. Pogreška ima polja error (kôd) i message.
ringtoneboolean | stringNefalseReproducira melodiju zvona tijekom povezivanja. true za zadanu melodiju zvona ili URL niz za prilagođeni zvuk.

Povratna vrijednost

Hook vraća objekt UseThunderPhoneReturn:

SvojstvoVrstaOpis
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Trenutačno stanje veze.
connect() => voidPokrenite glasovnu sesiju.
disconnect() => voidZavršite trenutačnu sesiju.
toggleMute() => voidUključite/isključite utišavanje mikrofona.
isMutedbooleanJe li mikrofon trenutačno utišan.
errorstring | undefinedPoruka o pogrešci kada je stanje 'error'.
agentNamestring | undefinedNaziv za prikaz povezanog agenta.
audioLevelnumberZastarjelo -- uvijek 0. Statično rezervirano mjesto zadržano radi kompatibilnosti sa starijim verzijama; nikada se ne ažurira. Umjesto toga pročitajte audioLevelRef.current.
audioLevelRefReact.RefObject<number>Promjenjiva referenca koja sadrži razinu zvuka u stvarnom vremenu (0--1) -- glasniji od glasa agenta i mikrofona posjetitelja -- ažurirana u svakom okviru animacije, izvan Reactova ciklusa renderiranja. Za glatke animacije bez zastajkivanja pročitajte audioLevelRef.current unutar petlji requestAnimationFrame ili ga uzorkujte u intervalima kada vam je vrijednost potrebna u stanju Reacta.
audioReactNodeNevidljivi element koji upravlja audiovezom -- mora se renderirati.

Korisničko sučelje koje reagira na zvuk

Ref audioLevelRef omogućuje vam razine zvuka pri brzini osvježavanja bez pokretanja ponovnog renderiranja Reacta, što ga čini idealnim za upravljanje glatkim vizualizacijama valnog oblika, pulsirajućim kuglama ili bilo kojom animacijom povezanom s razgovorom. Razina odražava glasniji izvor: glas agenta ili mikrofon posjetitelja.

Primjer valnog oblika

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>
  )
}

Primjer pulsirajuće kugle

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>
  )
}

Primjer indikatora govora

Za korisničko sučelje koje renderira React i mijenja se s glasnoćom -- poput oznake „govori” koja se temelji na pragu -- uzorkujte audioLevelRef.current u intervalima i pohranite rezultat u stanje:

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>
  )
}

Stanje automata

Svojstvo state prati ovaj životni ciklus:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
StanjeOpis
idleNema aktivne sesije. Spremno za pozivanje connect().
connectingSesija se uspostavlja. Onemogućite gumb za poziv tijekom ovog stanja.
connectedGlasovna sesija je aktivna. Korisnik razgovara s agentom.
disconnectedSesija je uredno završena. Automatski se vraća na idle nakon 1,5 sekundi.
errorNešto je pošlo po zlu. Poruku provjerite u phone.error. Stanje se ne briše samo od sebe -- ponovno pozivanje connect() pokreće novi pokušaj i poništava pogrešku.

Primjeri

S kontrolom utišavanja

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 melodijom zvona

Reproducirajte zvuk zvonjave tijekom povezivanja kako biste simulirali telefonski poziv:

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}
    </>
  )
}

Melodija zvona ponavlja se tijekom stanja connecting i postupno se stišava kada se agent poveže. Proslijedite true za ugrađenu zadanu melodiju zvona ili URL niz za upotrebu vlastite audiodatoteke.

S povratnim pozivima događaja

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}
    </>
  )
}

Potpuno prilagođeno sučelje

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>
  )
}

Savjeti

Uvijek renderirajte phone.audio

Element phone.audio nije vidljiv, ali je obavezan. Postavite ga bilo gdje u svoj JSX -- ne renderira vidljivi DOM, ali interno upravlja WebRTC audio vezom.

Onemogućite gumb tijekom povezivanja

Stanje connecting može trajati 1-3 sekunde. Onemogućite gumb za poziv tijekom tog stanja kako biste spriječili dvostruke pokušaje povezivanja.

Pravilno obradite stanje pogreške

Kada je stanje error, prikažite korisniku phone.error i zadržite omogućen gumb za poziv. Hook ne napušta stanje error samostalno -- ponovni poziv connect() pokreće novi pokušaj i briše prethodnu pogrešku.

Upotrebljavajte povratne funkcije za popratne učinke

Povratne funkcije onConnect, onDisconnect i onError idealne su za analitiku, zapisivanje ili pokretanje druge logike aplikacije bez anketiranja stanja.

Čitajte razine zvuka iz audioLevelRef

audioLevelRef jedini je aktivni izvor razine zvuka. Čitajte audioLevelRef.current unutar requestAnimationFrame za glatke animacije poput valnih oblika (čitanje refa ne uzrokuje ponovno renderiranje) ili ga uzorkujte u intervalima i pohranite rezultat u stanje za korisničko sučelje koje renderira React. Broj audioLevel zastario je i uvijek je 0 -- nemojte graditi logiku na njemu.