ThunderPhone 2.0 je tady.Začnete bez obchodníka, od 2 ¢/min.Přečíst oznámení

Widget

Headless Hook

Vytvořte plně vlastní hlasové uživatelské rozhraní pomocí React hooku useThunderPhone

Hook useThunderPhone vám poskytuje úplnou kontrolu nad uživatelským rozhraním, zatímco ThunderPhone spravuje hlasovou relaci, směrování zvuku a stav připojení. Použijte ho, když chcete plně vlastní uživatelské rozhraní -- vlastní tlačítka, rozvržení, animace a branding -- zatímco ThunderPhone se postará o vše na pozadí.

Kdy použít headless hook

Předpřipravená komponenta ThunderPhoneWidget pokrývá většinu případů použití, ale headless hook použijte, když potřebujete:

  • Zcela vlastní uživatelské rozhraní hovoru, které odpovídá designovému systému vaší aplikace
  • Vizualizace reagující na zvuk (křivky zvuku, koule, pulzující indikátory) řízené úrovněmi zvuku v reálném čase
  • Vlastní toky hovoru, jako jsou formuláře před hovorem, dotazníky po hovoru nebo integrovaný chat vedle hlasové komunikace
  • Integraci do existující knihovny komponent (Material UI, Chakra, Radix atd.)

Instalace

npm install @thunderphone/widget

Základní použití

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

Možnosti

Tyto možnosti předejte do useThunderPhone prostřednictvím UseThunderPhoneOptions:

MožnostTypPovinnéVýchozíPopis
publishableKeystringAno--Publikovatelný klíč API (pk_live_...). Agent se automaticky určí z konfigurace widgetu daného klíče.
apiBasestringNe'https://api.thunderphone.com/v1'Přepsání základní adresy URL API.
languagestringNe--Přepsání jazyka pro relaci -- kód jazyka nebo národní prostředí, například en, es nebo fr-FR. Pokud není nastaveno, použije se nakonfigurovaný jazyk agenta.
voicestringNe--Přepsání hlasu pro relaci -- název hlasu, například maria. Pokud není nastaveno, použije se nakonfigurovaný hlas agenta.
contextstringNe--Faktický kontext stránky nebo webu pro relaci předaný agentovi. Na straně serveru zkrácený na 12 000 znaků.
onConnect() => voidNe--Volá se při připojení hlasové relace.
onDisconnect() => voidNe--Volá se při ukončení relace.
onError(error) => voidNe--Volá se při chybách. Chyba obsahuje pole error (kód) a message.
ringtoneboolean | stringNefalsePřehrává vyzvánění během připojování. true pro výchozí vyzvánění nebo řetězec URL pro vlastní zvuk.

Návratová hodnota

Hook vrací objekt UseThunderPhoneReturn:

VlastnostTypPopis
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Aktuální stav připojení.
connect() => voidSpustí hlasovou relaci.
disconnect() => voidUkončí aktuální relaci.
toggleMute() => voidZapne nebo vypne ztlumení mikrofonu.
isMutedbooleanZda je mikrofon aktuálně ztlumený.
errorstring | undefinedChybová zpráva, když je stav 'error'.
agentNamestring | undefinedZobrazovaný název připojeného agenta.
audioLevelnumberZastaralé -- vždy 0. Statický zástupný symbol zachovaný kvůli zpětné kompatibilitě; nikdy se neaktualizuje. Místo toho čtěte audioLevelRef.current.
audioLevelRefReact.RefObject<number>Měnitelná reference obsahující úroveň zvuku v reálném čase (0--1) -- vyšší z hlasitosti hlasu agenta a mikrofonu návštěvníka -- aktualizovaná v každém animačním snímku mimo cyklus vykreslování Reactu. Pro plynulé animace bez zasekávání čtěte audioLevelRef.current uvnitř smyček requestAnimationFrame, nebo hodnotu vzorkujte v intervalu, když ji potřebujete ve stavu Reactu.
audioReactNodeNeviditelný prvek, který zajišťuje zvukové připojení -- musí být vykreslen.

Uživatelské rozhraní reagující na zvuk

Ref audioLevelRef poskytuje úrovně zvuku ve snímkové frekvenci bez vyvolání opětovného vykreslení Reactu, takže je ideální pro plynulé vizualizace zvukové vlny, pulzující koule nebo jakoukoli animaci navázanou na konverzaci. Úroveň odráží hlasitější ze dvou zdrojů: hlas agenta nebo mikrofon návštěvníka.

Příklad zvukové vlny

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

Příklad pulzující koule

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

Příklad indikátoru mluvení

Pro uživatelské rozhraní vykreslované Reactem, které se mění podle hlasitosti – například štítek „mluví“ založený na prahové hodnotě – načítejte audioLevelRef.current v intervalu a ukládejte výsledek do stavu:

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

Stavový automat

Vlastnost state prochází tímto životním cyklem:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
StavPopis
idleŽádná aktivní relace. Připraveno k volání connect().
connectingRelace se navazuje. V tomto stavu zakažte tlačítko volání.
connectedHlasová relace je aktivní. Uživatel mluví s agentem.
disconnectedRelace byla řádně ukončena. Po 1,5 sekundě se automaticky přepne zpět do stavu idle.
errorNěco se pokazilo. Zkontrolujte zprávu v phone.error. Stav se sám nevymaže -- opětovné volání connect() zahájí nový pokus a resetuje chybu.

Příklady

S ovládáním ztlumení

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 vyzváněním

Při připojování přehrajte vyzváněcí zvuk pro simulaci telefonního hovoru:

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

Vyzvánění se během stavu connecting opakuje a po připojení agenta postupně utichne. Pro vestavěné výchozí vyzvánění předejte hodnotu true, nebo řetězec URL pro použití vlastního zvukového souboru.

S obslužnými funkcemi událostí

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

Plně vlastní uživatelské rozhraní

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

Tipy

Vždy vykreslujte phone.audio

Prvek phone.audio je neviditelný, ale povinný. Umístěte jej kamkoli do JSX -- nevykresluje žádný viditelný DOM, ale interně spravuje zvukové připojení WebRTC.

Během připojování tlačítko deaktivujte

Stav connecting může trvat 1–3 sekundy. Během tohoto stavu deaktivujte tlačítko volání, abyste zabránili duplicitním pokusům o připojení.

Stav chyby zpracujte vhodně

Když je stav error, zobrazte uživateli phone.error a tlačítko volání ponechte aktivní. Hook stav error sám neopustí -- opětovné volání connect() zahájí nový pokus a vymaže předchozí chybu.

Pro vedlejší efekty používejte callbacky

Callbacky onConnect, onDisconnect a onError jsou ideální pro analytiku, protokolování nebo spuštění jiné logiky aplikace bez dotazování na stav.

Úrovně zvuku čtěte z audioLevelRef

audioLevelRef je jediný živý zdroj úrovně zvuku. Pro plynulé animace, například průběhy vln, čtěte audioLevelRef.current uvnitř requestAnimationFrame (čtení ref nezpůsobuje opětovné vykreslení), nebo jej vzorkujte v intervalu a výsledek ukládejte do stavu pro uživatelské rozhraní vykreslované Reactem. Číslo audioLevel je zastaralé a vždy má hodnotu 0 -- nestavte na něm logiku.