Headless Hook

useThunderPhone kabliukas suteikia jums visišką naudotojo sąsajos valdymą, o ThunderPhone tvarko balso sesiją, garso nukreipimą ir ryšio būseną. Naudokite jį, kai norite visiškai pritaikytos naudotojo sąsajos -- savo mygtukų, išdėstymų, animacijų ir prekės ženklo -- o ThunderPhone viskuo pasirūpina viduje.

Kada naudoti kabliuką be sąsajos

Iš anksto sukurtas ThunderPhoneWidget komponentas apima daugumą naudojimo atvejų, tačiau rinkitės kabliuką be sąsajos, kai reikia:


Diegimas

npm install @thunderphone/widget

Pagrindinis naudojimas

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

Parinktys

Perduokite šias parinktis į useThunderPhone naudodami UseThunderPhoneOptions:

ParinktisTipasBūtinaNumatytoji reikšmėAprašymas
publishableKeystringTaip--Viešasis API raktas (pk_live_...). Agentas automatiškai nustatomas pagal rakto valdiklio konfigūraciją.
apiBasestringNe'https://api.thunderphone.com/v1'API bazinio URL pakeitimas.
languagestringNe--Vienos sesijos kalbos pakeitimas -- kalbos kodas arba lokalė, pvz., en, es arba fr-FR. Jei nenustatyta, taikoma sukonfigūruota agento kalba.
voicestringNe--Vienos sesijos balso pakeitimas -- balso pavadinimas, pvz., maria. Jei nenustatyta, taikomas sukonfigūruotas agento balsas.
contextstringNe--Vienos sesijos faktinis puslapio arba svetainės kontekstas, perduodamas agentui. Serveryje sutrumpinamas iki 12 000 simbolių.
onConnect() => voidNe--Iškviečiama prisijungus balso sesijai.
onDisconnect() => voidNe--Iškviečiama pasibaigus sesijai.
onError(error) => voidNe--Iškviečiama įvykus klaidoms. Klaida turi laukus error (kodas) ir message.
ringtoneboolean | stringNefalseLeisti skambėjimo toną jungiantis. true numatytajam skambėjimo tonui arba URL eilutė pritaikytam garsui.

Grąžinama reikšmė

Hukas grąžina UseThunderPhoneReturn objektą:

SavybėTipasAprašymas
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Dabartinė ryšio būsena.
connect() => voidPradėkite balso seansą.
disconnect() => voidUžbaikite dabartinį seansą.
toggleMute() => voidĮjunkite arba išjunkite mikrofono nutildymą.
isMutedbooleanAr mikrofonas šiuo metu nutildytas.
errorstring | undefinedKlaidos pranešimas, kai būsena yra 'error'.
agentNamestring | undefinedPrijungto agento rodomas pavadinimas.
audioLevelnumberNebenaudojama -- visada 0. Statinė vietaženklė reikšmė, palikta siekiant atgalinio suderinamumo; ji niekada neatnaujinama. Vietoje to nuskaitykite audioLevelRef.current.
audioLevelRefReact.RefObject<number>Keičiamas ref, kuriame pateikiamas garso lygis realiuoju laiku (0--1) -- didesnis iš agento balso ir lankytojo mikrofono lygių -- atnaujinamas kiekviename animacijos kadre, ne React atvaizdavimo cikle. Sklandžioms, netrūkčiojančioms animacijoms nuskaitykite audioLevelRef.current requestAnimationFrame cikluose arba imkite reikšmės pavyzdžius intervalais, kai jos reikia React būsenoje.
audioReactNodeNematomas elementas, valdantis garso ryšį -- privalo būti atvaizduotas.

Į garsą reaguojanti sąsaja

Nuoroda audioLevelRef pateikia kadrų dažnio garso lygius nesukeldama React pakartotinių atvaizdavimų, todėl puikiai tinka sklandžioms bangos formos vizualizacijoms, pulsuojantiems rutuliams ar bet kuriai su pokalbiu susietai animacijai valdyti. Lygis atspindi, kuris garsas yra stipresnis: agento balsas ar lankytojo mikrofonas.

Bangos formos pavyzdys

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

Pulsuojančio rutulio pavyzdys

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

Kalbėjimo indikatoriaus pavyzdys

React atvaizduojamai sąsajai, kuri keičiasi pagal garsumą, pavyzdžiui, slenksčiu pagrįstai „kalbėjimo“ žymai, nuskaitykite audioLevelRef.current nustatytu intervalu ir išsaugokite rezultatą būsenoje:

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

Būsenų automatas

Savybė state veikia pagal šį gyvavimo ciklą:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
BūsenaAprašymas
idleNėra aktyvios sesijos. Parengta kviesti connect().
connectingKuriama sesija. Šios būsenos metu išjunkite skambinimo mygtuką.
connectedBalso sesija aktyvi. Naudotojas kalbasi su agentu.
disconnectedSesija baigėsi sėkmingai. Po 1,5 sekundės automatiškai grįžta į idle.
errorĮvyko klaida. Pranešimą tikrinkite phone.error. Būsena savaime neišsivalo -- pakartotinai iškvietus connect() pradedamas naujas bandymas ir klaida nustatoma iš naujo.

Pavyzdžiai

Su nutildymo valdymu

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

Su skambėjimo signalu

Leiskite skambėjimo garsą jungimosi metu, kad imituotumėte telefono skambutį:

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

Skambėjimo signalas kartojamas būsenos connecting metu ir nutyla, kai agentas prisijungia. Perduokite true, jei norite naudoti integruotą numatytąjį skambėjimo signalą, arba URL eilutę, jei norite naudoti savo garso failą.

Su įvykių atgaliniais iškvietimais

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

Visiškai pritaikyta vartotojo sąsaja

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

Patarimai

Visada pateikite phone.audio

Elementas phone.audio yra nematomas, tačiau būtinas. Įdėkite jį bet kur JSX kode -- jis nepateikia matomo DOM, bet viduje valdo WebRTC garso ryšį.

Jungiantis išjunkite mygtuką

Būsena connecting gali trukti 1–3 sekundes. Šios būsenos metu išjunkite skambinimo mygtuką, kad išvengtumėte pasikartojančių prisijungimo bandymų.

Tinkamai apdorokite klaidos būseną

Kai būsena yra error, parodykite naudotojui phone.error ir palikite skambinimo mygtuką įjungtą. Kabliukas savarankiškai neišeina iš error būsenos -- dar kartą iškvietus connect(), pradedamas naujas bandymas ir išvaloma ankstesnė klaida.

Šaliniams veiksmams naudokite atgalinius iškvietimus

Atgaliniai iškvietimai onConnect, onDisconnect ir onError idealiai tinka analitikai, žurnalų įrašams arba kitai programos logikai suaktyvinti, neapklausiant būsenos.

Garso lygius nuskaitykite iš audioLevelRef

audioLevelRef yra vienintelis tiesioginis garso lygio šaltinis. Kad animacijos, pvz., bangų formos, būtų sklandžios, nuskaitykite audioLevelRef.currentrequestAnimationFrame (ref nuskaitymas nesukelia pakartotinio atvaizdavimo), arba imkite jo reikšmę intervalais ir saugokite rezultatą būsenoje, skirtoje React atvaizduojamai sąsajai. Skaičius audioLevel yra pasenęs ir visada lygus 0 -- nekurdami logikos juo nesiremkite.