Headless Hook

Кука useThunderPhone Вам пружа потпуну контролу над корисничким интерфејсом док ThunderPhone управља гласовном сесијом, усмеравањем звука и стањем везе. Користите је када желите потпуно прилагођен кориснички интерфејс -- сопствене дугмад, распореде, анимације и брендирање -- док ThunderPhone обавља све у позадини.

Када користити куку без интерфејса

Унапред изграђена компонента ThunderPhoneWidget покрива већину случајева употребе, али користите куку без интерфејса када Вам је потребно:


Инсталација

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:

ОпцијаТипОбавезноПодразумеваноОпис
publishableKeystringДа--Јавни API кључ (pk_live_...). Агент се аутоматски одређује из конфигурације виџета тог кључа.
apiBasestringНе'https://api.thunderphone.com/v1'Замена основног URL-а API-ја.
languagestringНе--Замена језика по сесији -- кôд језика или локализација као што су en, es или fr-FR. Када није подешено, примењује се конфигурисани језик агента.
voicestringНе--Замена гласа по сесији -- назив гласа као што је maria. Када није подешено, примењује се конфигурисани глас агента.
contextstringНе--Чињенични контекст странице или сајта по сесији који се прослеђује агенту. На страни сервера скраћује се на 12.000 знакова.
onConnect() => voidНе--Позива се када се гласовна сесија повеже.
onDisconnect() => voidНе--Позива се када се сесија заврши.
onError(error) => voidНе--Позива се при грешкама. Грешка има поља error (кôд) и message.
ringtoneboolean | stringНеfalseРепродукује мелодију звона током повезивања. true за подразумевану мелодију звона или URL ниску за прилагођени звук.

Повратна вредност

Хук враћа објекат UseThunderPhoneReturn:

СвојствоТипОпис
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Тренутно стање везе.
connect() => voidПокрените гласовну сесију.
disconnect() => voidЗавршите тренутну сесију.
toggleMute() => voidУкључите или искључите утишавање микрофона.
isMutedbooleanДа ли је микрофон тренутно утишан.
errorstring | undefinedПорука о грешци када је стање 'error'.
agentNamestring | undefinedПриказано име повезаног агента.
audioLevelnumberЗастарело -- увек 0. Статички чувар места задржан ради компатибилности са старијим верзијама; никада се не ажурира. Уместо тога читајте audioLevelRef.current.
audioLevelRefReact.RefObject<number>Променљива референца која садржи ниво звука у реалном времену (0--1) -- јачи од гласа агента и микрофона посетиоца -- ажурира се у сваком анимационом кадру, ван React циклуса рендеровања. Читајте audioLevelRef.current унутар requestAnimationFrame петљи за глатке анимације без застоја или га узоркујте у интервалима када вам је вредност потребна у React стању.
audioReactNodeНевидљиви елемент који управља аудио-везом -- мора бити рендерован.

Кориснички интерфејс који реагује на звук

Референца 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 -- немојте градити логику на њему.