ThunderPhone 2.0 este acum disponibil.Îl configurați singur, de la 2 ¢/min.Citiți anunțul

Widget

Hook headless

Creați o interfață vocală complet personalizată cu hook-ul React useThunderPhone

Hook-ul useThunderPhone vă oferă control complet asupra interfeței cu utilizatorul, în timp ce ThunderPhone gestionează sesiunea vocală, rutarea audio și starea conexiunii. Utilizați-l când doriți o interfață complet personalizată -- propriile butoane, aspecte, animații și elemente de branding -- în timp ce ThunderPhone gestionează totul în fundal.

Când să utilizați hook-ul Headless

Componenta predefinită ThunderPhoneWidget acoperă majoritatea cazurilor de utilizare, însă utilizați hook-ul headless când aveți nevoie de:

  • O interfață de apel complet personalizată, care se potrivește cu sistemul de design al aplicației dumneavoastră
  • Vizualizări reactive la audio (forme de undă, sfere, indicatori pulsați) bazate pe nivelurile audio în timp real
  • Fluxuri de apel personalizate, precum formulare înainte de apel, sondaje după apel sau chat integrat alături de voce
  • Integrare într-o bibliotecă de componente existentă (Material UI, Chakra, Radix etc.)

Instalare

npm install @thunderphone/widget

Utilizare de bază

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

Opțiuni

Transmiteți aceste opțiuni către useThunderPhone prin UseThunderPhoneOptions:

OpțiuneTipObligatoriuImplicitDescriere
publishableKeystringDa--Cheie API publicabilă (pk_live_...). Agentul este determinat automat din configurația widget-ului cheii.
apiBasestringNu'https://api.thunderphone.com/v1'Suprascrierea URL-ului de bază al API-ului.
languagestringNu--Suprascrierea limbii pentru fiecare sesiune -- un cod de limbă sau o configurație regională, precum en, es sau fr-FR. Dacă nu este setată, se aplică limba configurată a agentului.
voicestringNu--Suprascrierea vocii pentru fiecare sesiune -- un nume de voce, precum maria. Dacă nu este setată, se aplică vocea configurată a agentului.
contextstringNu--Context factual al paginii sau site-ului pentru fiecare sesiune, transmis agentului. Este trunchiat pe server la 12.000 de caractere.
onConnect() => voidNu--Apelată când se conectează sesiunea vocală.
onDisconnect() => voidNu--Apelată când se încheie sesiunea.
onError(error) => voidNu--Apelată la erori. Eroarea are câmpurile error (cod) și message.
ringtoneboolean | stringNufalseRedați un ton de apel în timpul conectării. true pentru tonul de apel implicit sau un șir URL pentru audio personalizat.

Valoare returnată

Hook-ul returnează un obiect UseThunderPhoneReturn:

ProprietateTipDescriere
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Starea curentă a conexiunii.
connect() => voidPorniți o sesiune vocală.
disconnect() => voidÎncheiați sesiunea curentă.
toggleMute() => voidActivați/dezactivați dezactivarea sunetului pentru microfon.
isMutedbooleanIndică dacă microfonul este dezactivat în prezent.
errorstring | undefinedMesaj de eroare când starea este 'error'.
agentNamestring | undefinedNumele afișat al agentului conectat.
audioLevelnumberDepreciat -- întotdeauna 0. Un substituent static păstrat pentru compatibilitate cu versiunile anterioare; nu se actualizează niciodată. Citiți în schimb audioLevelRef.current.
audioLevelRefReact.RefObject<number>O referință mutabilă care conține nivelul audio în timp real (0--1) -- cel mai ridicat dintre vocea agentului și microfonul vizitatorului -- actualizată la fiecare cadru de animație, în afara ciclului de randare React. Citiți audioLevelRef.current în buclele requestAnimationFrame pentru animații fluide, fără sacadări, sau eșantionați-l la un interval atunci când aveți nevoie de valoare în starea React.
audioReactNodeElement invizibil care gestionează conexiunea audio -- trebuie randat.

Interfață reactivă la audio

Referința audioLevelRef vă oferă niveluri audio la rata cadrelor fără a declanșa re-randări React, ceea ce o face ideală pentru vizualizări fluide ale formei de undă, sfere pulsante sau orice animație legată de conversație. Nivelul reflectă sursa mai puternică: vocea agentului sau microfonul vizitatorului.

Exemplu de formă de undă

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

Exemplu de sferă pulsantă

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

Exemplu de indicator pentru vorbire

Pentru o interfață redată de React care se modifică în funcție de volum -- precum o insignă „vorbește” bazată pe prag -- eșantionați audioLevelRef.current la un interval și stocați rezultatul în stare:

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

Mașina de stări

Proprietatea state urmează acest ciclu de viață:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
StareDescriere
idleNicio sesiune activă. Gata pentru apelarea connect().
connectingSesiunea este în curs de stabilire. Dezactivați butonul de apel în această stare.
connectedSesiunea vocală este activă. Utilizatorul vorbește cu agentul.
disconnectedSesiunea s-a încheiat corect. Revine automat la idle după 1,5 secunde.
errorCeva nu a funcționat. Verificați phone.error pentru mesaj. Starea nu se șterge automat -- apelarea din nou a connect() începe o nouă încercare și resetează eroarea.

Exemple

Cu controlul dezactivării sunetului

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

Cu ton de apel

Redați un sunet de apel în timpul conectării pentru a simula un apel telefonic:

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

Tonul de apel se redă în buclă în starea connecting și se estompează când agentul se conectează. Transmiteți true pentru tonul de apel implicit integrat sau un șir URL pentru a utiliza propriul fișier audio.

Cu callback-uri de evenimente

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

Interfață UI complet personalizată

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

Sfaturi

Redați întotdeauna phone.audio

Elementul phone.audio este invizibil, dar obligatoriu. Plasați-l oriunde în JSX — nu redă niciun DOM vizibil, dar gestionează intern conexiunea audio WebRTC.

Dezactivați butonul în timpul conectării

Starea connecting poate dura 1-3 secunde. Dezactivați butonul de apel în această stare pentru a preveni încercările de conectare duplicate.

Gestionați elegant starea de eroare

Când starea este error, afișați utilizatorului phone.error și păstrați activat butonul de apel. Hook-ul nu părăsește singur starea error — apelarea din nou a connect() începe o încercare nouă și șterge eroarea anterioară.

Utilizați callback-uri pentru efecte secundare

Callback-urile onConnect, onDisconnect și onError sunt ideale pentru analiză, jurnalizare sau declanșarea altor logici ale aplicației fără interogarea repetată a stării.

Citiți nivelurile audio din audioLevelRef

audioLevelRef este singura sursă live pentru nivelul audio. Citiți audioLevelRef.current în interiorul requestAnimationFrame pentru animații fluide, cum ar fi formele de undă (citirea unui ref nu provoacă rerandări), sau eșantionați-l la un interval și stocați rezultatul în stare pentru interfața redată de React. Numărul audioLevel este depreciat și este întotdeauna 0 — nu construiți logică bazată pe acesta.