ThunderPhone 2.0 er lansert.Kom i gang selv, fra 2 ¢/min.Les mer om lanseringen

Widget

Headless Hook

Bygg et fullstendig tilpasset stemmegrensesnitt med React-hooken useThunderPhone

useThunderPhone-hooken gir deg full kontroll over brukergrensesnittet mens ThunderPhone håndterer stemmeøkten, lydrutingen og tilkoblingsstatusen. Bruk den når du vil ha et helt tilpasset brukergrensesnitt -- egne knapper, oppsett, animasjoner og profilering -- mens ThunderPhone håndterer alt i bakgrunnen.

Når du skal bruke Headless-hooken

Den ferdigbygde ThunderPhoneWidget-komponenten dekker de fleste brukstilfeller, men bruk headless-hooken når du trenger:

  • Et helt tilpasset samtalegrensesnitt som matcher designsystemet i appen din
  • Lydreaktive visualiseringer (bølgeformer, kuler, pulserende indikatorer) drevet av lydnivåer i sanntid
  • Tilpassede samtaleflyter som skjemaer før samtalen, undersøkelser etter samtalen eller innebygd chat ved siden av stemmen
  • Integrasjon i et eksisterende komponentbibliotek (Material UI, Chakra, Radix osv.)

Installasjon

npm install @thunderphone/widget

Grunnleggende bruk

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

Alternativer

Send disse alternativene til useThunderPhone via UseThunderPhoneOptions:

AlternativTypePåkrevdStandardBeskrivelse
publishableKeystringJa--Publiserbar API-nøkkel (pk_live_...). Stemmeagenten bestemmes automatisk fra nøkkelens widgetkonfigurasjon.
apiBasestringNei'https://api.thunderphone.com/v1'Overstyring av API-basis-URL.
languagestringNei--Språkoverstyring per økt -- en språkkode eller lokalitet som en, es eller fr-FR. Når den ikke er angitt, brukes stemmeagentens konfigurerte språk.
voicestringNei--Stemmeoverstyring per økt -- et stemmenavn som maria. Når den ikke er angitt, brukes stemmeagentens konfigurerte stemme.
contextstringNei--Faktabasert side- eller nettstedskontekst per økt som sendes til stemmeagenten. Avkortes på serversiden til 12 000 tegn.
onConnect() => voidNei--Kalles når stemmeøkten kobles til.
onDisconnect() => voidNei--Kalles når økten avsluttes.
onError(error) => voidNei--Kalles ved feil. Feilen har feltene error (kode) og message.
ringtoneboolean | stringNeifalseSpill av en ringetone mens tilkoblingen opprettes. true for standardringetonen, eller en URL-streng for egendefinert lyd.

Returverdi

Hooken returnerer et UseThunderPhoneReturn-objekt:

EgenskapTypeBeskrivelse
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Gjeldende tilkoblingsstatus.
connect() => voidStart en stemmeøkt.
disconnect() => voidAvslutt gjeldende økt.
toggleMute() => voidSlå mikrofonens demping av/på.
isMutedbooleanOm mikrofonen er dempet nå.
errorstring | undefinedFeilmelding når statusen er 'error'.
agentNamestring | undefinedVisningsnavn for den tilkoblede agenten.
audioLevelnumberUtdatert -- alltid 0. En statisk plassholder beholdt for bakoverkompatibilitet; den oppdateres aldri. Les audioLevelRef.current i stedet.
audioLevelRefReact.RefObject<number>En muterbar ref som inneholder lydnivået i sanntid (0--1) -- det høyeste av stemmeagentens stemme og den besøkendes mikrofon -- oppdatert på hver animasjonsramme, utenfor Reacts rendringssyklus. Les audioLevelRef.current i requestAnimationFrame-løkker for jevne animasjoner uten hakking, eller hent verdien med et intervall når du trenger den i React-status.
audioReactNodeUsynlig element som håndterer lydtilkoblingen -- må rendres.

Lydreaktivt brukergrensesnitt

Ref-en audioLevelRef gir deg lydnivåer med bildefrekvens uten å utløse React-gjengivelser, noe som gjør den ideell for jevne bølgeformvisualiseringer, pulserende kuler eller enhver animasjon knyttet til samtalen. Nivået gjenspeiler det som er høyest: agentens stemme eller den besøkendes mikrofon.

Eksempel på bølgeform

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

Eksempel på pulserende kule

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

Eksempel på taleindikator

For React-gjengitt brukergrensesnitt som endres med volumet -- for eksempel et terskelbasert «snakker»-merke -- les audioLevelRef.current med et intervall og lagre resultatet i tilstand:

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

Tilstandsmaskin

Egenskapen state følger denne livssyklusen:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
TilstandBeskrivelse
idleIngen aktiv økt. Klar til å kalle connect().
connectingØkten opprettes. Deaktiver ringeknappen i denne tilstanden.
connectedStemmeøkten er aktiv. Brukeren snakker med agenten.
disconnectedØkten er avsluttet på en ryddig måte. Går automatisk tilbake til idle etter 1,5 sekunder.
errorNoe gikk galt. Sjekk phone.error for meldingen. Tilstanden tømmes ikke av seg selv -- å kalle connect() på nytt starter et nytt forsøk og tilbakestiller feilen.

Eksempler

Med dempekontroll

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 ringetone

Spill av en ringelyd mens tilkoblingen opprettes for å simulere en telefonsamtale:

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

Ringetonen gjentas mens statusen er connecting og tones ut når stemmeagenten kobler til. Send inn true for den innebygde standardringetonen, eller en URL-streng for å bruke din egen lydfil.

Med hendelsestilbakekall

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

Fullt tilpasset brukergrensesnitt

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

Render alltid phone.audio

Elementet phone.audio er usynlig, men nødvendig. Plasser det hvor som helst i JSX-en din -- det renderer ingen synlig DOM, men håndterer WebRTC-lydtilkoblingen internt.

Deaktiver knappen mens tilkoblingen opprettes

Tilstanden connecting kan vare i 1–3 sekunder. Deaktiver ringeknappen i denne tilstanden for å forhindre doble tilkoblingsforsøk.

Håndter feiltilstanden på en god måte

Når tilstanden er error, vis phone.error til brukeren og hold ringeknappen aktivert. Hooken forlater ikke error-tilstanden av seg selv -- å kalle connect() på nytt starter et nytt forsøk og fjerner den forrige feilen.

Bruk tilbakeringinger for sideeffekter

Tilbakeringingene onConnect, onDisconnect og onError er ideelle for analyse, logging eller utløsing av annen applikasjonslogikk uten å polle tilstanden.

Les lydnivåer fra audioLevelRef

audioLevelRef er den eneste direkte kilden til lydnivå. Les audioLevelRef.current inne i requestAnimationFrame for jevne animasjoner som bølgeformer (å lese en ref forårsaker ikke ny rendering), eller hent en prøve med jevne mellomrom og lagre resultatet i tilstanden for React-rendert brukergrensesnitt. Tallet audioLevel er utdatert og alltid 0 -- ikke bygg logikk på det.