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() започва нов опит и изчиства предишната грешка.

Използвайте callback функции за странични ефекти

Callback функциите onConnect, onDisconnect и onError са подходящи за анализи, регистриране или задействане на друга логика на приложението без периодична проверка на състоянието.

Четете нивата на аудиото от audioLevelRef

audioLevelRef е единственият източник на аудио ниво в реално време. Четете audioLevelRef.current в requestAnimationFrame за плавни анимации, като вълнови форми (четенето на ref не предизвиква повторно рендиране), или го проверявайте през интервал и съхранявайте резултата в state за интерфейс, рендиран от React. Числото audioLevel е отхвърлено и винаги е 0 -- не изграждайте логика върху него.