ThunderPhone 2.0 je tu.Začnite sami, už od 2 ¢/min.Prečítať oznámenie

Widget

Headless Hook

Vytvorte úplne vlastné hlasové používateľské rozhranie pomocou React hooku useThunderPhone

Hook useThunderPhone vám poskytuje úplnú kontrolu nad používateľským rozhraním, zatiaľ čo ThunderPhone spravuje hlasovú reláciu, smerovanie zvuku a stav pripojenia. Použite ho, keď chcete úplne vlastné UI -- vlastné tlačidlá, rozloženia, animácie a branding -- zatiaľ čo ThunderPhone zabezpečuje všetko na pozadí.

Kedy použiť hook bez vlastného UI

Predpripravený komponent ThunderPhoneWidget pokrýva väčšinu prípadov použitia, ale hook bez vlastného UI použite, keď potrebujete:

  • Úplne vlastné UI hovoru, ktoré zodpovedá dizajnovému systému vašej aplikácie
  • Vizualizácie reagujúce na zvuk (priebeh zvukovej vlny, gule, pulzujúce indikátory) riadené úrovňami zvuku v reálnom čase
  • Vlastné toky hovorov, ako sú formuláre pred hovorom, prieskumy po hovore alebo vložený chat popri hlase
  • Integráciu do existujúcej knižnice komponentov (Material UI, Chakra, Radix atď.)

Inštalácia

npm install @thunderphone/widget

Základné použitie

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

Tieto možnosti odovzdajte funkcii useThunderPhone prostredníctvom UseThunderPhoneOptions:

MožnosťTypPovinnéPredvolenéPopis
publishableKeystringÁno--Verejný kľúč API (pk_live_...). Hlasový agent sa automaticky určí z konfigurácie widgetu kľúča.
apiBasestringNie'https://api.thunderphone.com/v1'Prepísanie základnej URL API.
languagestringNie--Prepísanie jazyka pre reláciu -- kód jazyka alebo miestne nastavenie, napríklad en, es alebo fr-FR. Keď nie je nastavené, použije sa nakonfigurovaný jazyk hlasového agenta.
voicestringNie--Prepísanie hlasu pre reláciu -- názov hlasu, napríklad maria. Keď nie je nastavené, použije sa nakonfigurovaný hlas hlasového agenta.
contextstringNie--Vecný kontext stránky alebo webu pre reláciu odovzdaný hlasovému agentovi. Na strane servera sa skráti na 12 000 znakov.
onConnect() => voidNie--Volá sa po pripojení hlasovej relácie.
onDisconnect() => voidNie--Volá sa po ukončení relácie.
onError(error) => voidNie--Volá sa pri chybách. Chyba obsahuje polia error (kód) a message.
ringtoneboolean | stringNiefalsePrehrať vyzváňací tón počas pripájania. true použije predvolený vyzváňací tón alebo zadajte reťazec URL pre vlastný zvuk.

Návratová hodnota

Hook vracia objekt UseThunderPhoneReturn:

VlastnosťTypPopis
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Aktuálny stav pripojenia.
connect() => voidSpustí hlasovú reláciu.
disconnect() => voidUkončí aktuálnu reláciu.
toggleMute() => voidPrepne stlmenie mikrofónu.
isMutedbooleanUrčuje, či je mikrofón momentálne stlmený.
errorstring | undefinedChybové hlásenie, keď je stav 'error'.
agentNamestring | undefinedZobrazovaný názov pripojeného agenta.
audioLevelnumberZastarané -- vždy 0. Statický zástupný údaj zachovaný kvôli spätnej kompatibilite; nikdy sa neaktualizuje. Namiesto toho čítajte audioLevelRef.current.
audioLevelRefReact.RefObject<number>Meniteľná referencia obsahujúca úroveň zvuku v reálnom čase (0--1) -- hlasnejšiu z hlasu agenta a mikrofónu návštevníka -- aktualizovaná pri každom animačnom snímku mimo cyklu vykresľovania Reactu. Pre plynulé animácie bez zasekávania čítajte audioLevelRef.current v slučkách requestAnimationFrame, alebo hodnotu vzorkujte v intervale, keď ju potrebujete v stave Reactu.
audioReactNodeNeviditeľný prvok, ktorý spracúva zvukové pripojenie -- musí sa vykresliť.

Používateľské rozhranie reagujúce na zvuk

Referencia audioLevelRef poskytuje úrovne zvuku pri každom snímku bez spúšťania opätovného vykresľovania v Reacte, takže je ideálna na plynulé vizualizácie priebehu zvuku, pulzujúce gule alebo akúkoľvek animáciu naviazanú na konverzáciu. Úroveň odráža hlasnejší zdroj: hlas agenta alebo mikrofón návštevníka.

Príklad priebehu zvuku

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

Príklad pulzujúcej gule

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

Príklad indikátora hovorenia

Pre používateľské rozhranie vykresľované v Reacte, ktoré sa mení podľa hlasitosti -- napríklad odznak „hovorí“ založený na prahovej hodnote -- načítavajte audioLevelRef.current v intervaloch a výsledok ukladajte 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

Vlastnosť state prechádza týmto životným cyklom:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
StavPopis
idleŽiadna aktívna relácia. Pripravené na volanie connect().
connectingRelácia sa vytvára. Počas tohto stavu deaktivujte tlačidlo hovoru.
connectedHlasová relácia je aktívna. Používateľ hovorí s agentom.
disconnectedRelácia sa úspešne skončila. Po 1,5 sekunde sa automaticky prepne späť na idle.
errorNiečo sa pokazilo. Správu skontrolujte v phone.error. Stav sa nevymaže sám -- opätovné volanie connect() spustí nový pokus a resetuje chybu.

Príklady

S ovládaním stlmenia

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

So zvonením

Počas pripájania prehrajte zvuk zvonenia na simuláciu telefonické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}
    </>
  )
}

Zvonenie sa opakuje počas stavu connecting a po pripojení agenta postupne zoslabne. Odovzdajte true pre predvolené vstavané zvonenie alebo reťazec s adresou URL, ak chcete použiť vlastný zvukový súbor.

So spätnými volaniami udalostí

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

Plne vlastné používateľské rozhranie

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 vykreslite phone.audio

Prvok phone.audio je neviditeľný, ale povinný. Umiestnite ho kamkoľvek do svojho JSX -- nevykresľuje žiadny viditeľný DOM, ale interne spravuje zvukové pripojenie WebRTC.

Počas pripájania deaktivujte tlačidlo

Stav connecting môže trvať 1 až 3 sekundy. Počas tohto stavu deaktivujte tlačidlo hovoru, aby ste zabránili duplicitným pokusom o pripojenie.

Správne spracujte chybový stav

Keď je stav error, zobrazte používateľovi phone.error a ponechajte tlačidlo hovoru aktívne. Hook neopustí stav error sám -- opätovné volanie connect() spustí nový pokus a vymaže predchádzajúcu chybu.

Na vedľajšie účinky používajte spätné volania

Spätné volania onConnect, onDisconnect a onError sú ideálne na analytiku, zaznamenávanie alebo spúšťanie inej logiky aplikácie bez pravidelného kontrolovania stavu.

Čítajte úrovne zvuku z audioLevelRef

audioLevelRef je jediný živý zdroj úrovne zvuku. Ak chcete plynulé animácie, napríklad zvukové vlny, čítajte audioLevelRef.current v rámci requestAnimationFrame (čítanie ref nespôsobuje opätovné vykreslenie), alebo ho vzorkujte v intervale a výsledok uložte do stavu pre používateľské rozhranie vykresľované Reactom. Číslo audioLevel je zastarané a vždy má hodnotu 0 -- nestavajte na ňom logiku.