ThunderPhone 2.0 är här.Kom igång själv, från 2 cent/minut.Läs lanseringsnyheten

Widget

Headless Hook

Bygg ett helt anpassat röstgränssnitt med React-hooken useThunderPhone

Hooken useThunderPhone ger dig fullständig kontroll över användargränssnittet medan ThunderPhone hanterar röstsessionen, ljudroutningen och anslutningsstatusen. Använd den när du vill ha ett helt anpassat UI -- egna knappar, layouter, animationer och varumärkesprofilering -- medan ThunderPhone hanterar allt i bakgrunden.

När du ska använda den headlessa hooken

Den färdiga komponenten ThunderPhoneWidget täcker de flesta användningsfall, men använd den headlessa hooken när du behöver:

  • Ett helt anpassat samtals-UI som matchar appens designsystem
  • Ljudreaktiva visualiseringar (vågformer, klot, pulserande indikatorer) som styrs av ljudnivåer i realtid
  • Anpassade samtalsflöden, till exempel formulär före samtalet, enkäter efter samtalet eller integrerad chatt vid sidan av röst
  • Integration i ett befintligt komponentbibliotek (Material UI, Chakra, Radix osv.)

Installation

npm install @thunderphone/widget

Grundläggande användning

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

Alternativ

Skicka dessa alternativ till useThunderPhone via UseThunderPhoneOptions:

AlternativTypKrävsStandardBeskrivning
publishableKeystringJa--Publicerbar API-nyckel (pk_live_...). Agenten bestäms automatiskt från nyckelns widgetkonfiguration.
apiBasestringNej'https://api.thunderphone.com/v1'Åsidosättning av API-bas-URL.
languagestringNej--Åsidosättning av språk per session -- en språkkod eller språkvariant som en, es eller fr-FR. När den inte är angiven används agentens konfigurerade språk.
voicestringNej--Åsidosättning av röst per session -- ett röstnamn som maria. När den inte är angiven används agentens konfigurerade röst.
contextstringNej--Faktabaserad sid- eller webbplatskontext per session som skickas till agenten. Avkortas på serversidan till 12 000 tecken.
onConnect() => voidNej--Anropas när röstsessionen ansluts.
onDisconnect() => voidNej--Anropas när sessionen avslutas.
onError(error) => voidNej--Anropas vid fel. Felet har fälten error (kod) och message.
ringtoneboolean | stringNejfalseSpela en ringsignal medan anslutning sker. true för standardringsignalen eller en URL-sträng för anpassat ljud.

Returvärde

Hooken returnerar ett UseThunderPhoneReturn-objekt:

EgenskapTypBeskrivning
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Aktuellt anslutningsläge.
connect() => voidStarta en röstsession.
disconnect() => voidAvsluta den aktuella sessionen.
toggleMute() => voidVäxla mikrofonens avstängning på/av.
isMutedbooleanOm mikrofonen för närvarande är avstängd.
errorstring | undefinedFelmeddelande när läget är 'error'.
agentNamestring | undefinedVisningsnamn för den anslutna agenten.
audioLevelnumberUtfasad -- alltid 0. En statisk platshållare som behålls för bakåtkompatibilitet; den uppdateras aldrig. Läs audioLevelRef.current i stället.
audioLevelRefReact.RefObject<number>En föränderlig ref som innehåller ljudnivån i realtid (0--1) -- den högsta av agentens röst och besökarens mikrofon -- uppdaterad i varje animationsbildruta, utanför Reacts renderingscykel. Läs audioLevelRef.current i requestAnimationFrame-loopar för smidiga animationer utan hack, eller hämta värdet med ett intervall när du behöver det i React-tillståndet.
audioReactNodeOsynligt element som hanterar ljudanslutningen -- måste renderas.

Ljudreaktivt användargränssnitt

Referensen audioLevelRef ger dig ljudnivåer i bildfrekvens utan att utlösa React-omrenderingar, vilket gör den idealisk för att styra jämna vågformsvisualiseringar, pulserande sfärer eller andra animationer som är kopplade till samtalet. Nivån återspeglar det som är högst: agentens röst eller besökarens mikrofon.

Exempel på vågform

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

Exempel på pulserande sfär

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

Exempel på talindikator

För React-renderade användargränssnitt som ändras med volymen -- till exempel en tröskelbaserad indikator för ”talar” -- läser du av audioLevelRef.current med ett intervall och sparar resultatet i state:

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

Tillståndsmaskin

Egenskapen state följer denna livscykel:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
TillståndBeskrivning
idleIngen aktiv session. Redo att anropa connect().
connectingSessionen upprättas. Inaktivera samtalsknappen under detta tillstånd.
connectedRöstsessionen är aktiv. Användaren pratar med agenten.
disconnectedSessionen har avslutats utan fel. Övergår automatiskt tillbaka till idle efter 1,5 sekunder.
errorNågot gick fel. Kontrollera phone.error för meddelandet. Tillståndet rensas inte av sig självt -- att anropa connect() igen startar ett nytt försök och återställer felet.

Exempel

Med ljudavstängningskontroll

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

Med ringsignal

Spela upp ett ringsignal-ljud medan anslutningen upprättas för att simulera ett telefonsamtal:

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

Ringsignalen loopas under tillståndet connecting och tonas ut när agenten ansluter. Skicka true för den inbyggda standardringsignalen, eller en URL-sträng för att använda din egen ljudfil.

Med händelseåteranrop

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

Helt anpassat användargränssnitt

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

Tips

Rendera alltid phone.audio

Elementet phone.audio är osynligt men krävs. Placera det var som helst i din JSX -- det renderar ingen synlig DOM men hanterar WebRTC-ljudanslutningen internt.

Inaktivera knappen under anslutning

Tillståndet connecting kan vara i 1–3 sekunder. Inaktivera samtalsknappen under detta tillstånd för att förhindra dubbla anslutningsförsök.

Hantera feltillståndet smidigt

När tillståndet är error visar du phone.error för användaren och låter samtalsknappen vara aktiverad. Hooken lämnar inte tillståndet error på egen hand -- om du anropar connect() igen startar ett nytt försök och det tidigare felet rensas.

Använd callbacks för bieffekter

Callbackarna onConnect, onDisconnect och onError är idealiska för analys, loggning eller för att utlösa annan programlogik utan att polla tillståndet.

Läs ljudnivåer från audioLevelRef

audioLevelRef är den enda källan för ljudnivåer i realtid. Läs audioLevelRef.current inuti requestAnimationFrame för mjuka animationer som vågformer (att läsa en ref orsakar inga omrenderingar), eller sampla den med ett intervall och lagra resultatet i state för React-renderat gränssnitt. Talet audioLevel är föråldrat och alltid 0 -- bygg inte logik på det.