ThunderPhone 2.0 er lanceret.Selvbetjening fra 2 cent/min.Læs mere om lanceringen

Widget

Headless Hook

Byg en fuldt tilpasset stemmegrænseflade med React-hooken useThunderPhone

Hooken useThunderPhone giver dig fuld kontrol over brugergrænsefladen, mens ThunderPhone håndterer stemmesessionen, lydrouting og forbindelsestilstand. Brug den, når du vil have et fuldt tilpasset UI -- dine egne knapper, layouts, animationer og branding -- mens ThunderPhone håndterer alt i baggrunden.

Hvornår du skal bruge headless-hooken

Den færdigbyggede komponent ThunderPhoneWidget dækker de fleste anvendelsestilfælde, men brug headless-hooken, når du har brug for:

  • Et helt tilpasset opkalds-UI, der matcher din apps designsystem
  • Lydreaktive visualiseringer (bølgeformer, kugler, pulserende indikatorer) drevet af lydniveauer i realtid
  • Tilpassede opkaldsforløb såsom formularer før opkaldet, spørgeundersøgelser efter opkaldet eller integreret chat ved siden af stemmen
  • Integration i et eksisterende komponentbibliotek (Material UI, Chakra, Radix osv.)

Installation

npm install @thunderphone/widget

Grundlæggende brug

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

Indstillinger

Send disse indstillinger til useThunderPhone via UseThunderPhoneOptions:

IndstillingTypePåkrævetStandardBeskrivelse
publishableKeystringJa--Offentlig API-nøgle (pk_live_...). Agenten bestemmes automatisk ud fra nøglens widgetkonfiguration.
apiBasestringNej'https://api.thunderphone.com/v1'Tilsidesættelse af API-basis-URL.
languagestringNej--Tilsidesættelse af sprog pr. session -- en sprogkode eller lokalitet som en, es eller fr-FR. Når den ikke er angivet, bruges agentens konfigurerede sprog.
voicestringNej--Tilsidesættelse af stemme pr. session -- et stemmenavn som maria. Når den ikke er angivet, bruges agentens konfigurerede stemme.
contextstringNej--Faktuel side- eller sitekontekst pr. session, der sendes til agenten. Afkortes på serversiden til 12.000 tegn.
onConnect() => voidNej--Kaldes, når stemmesessionen opretter forbindelse.
onDisconnect() => voidNej--Kaldes, når sessionen afsluttes.
onError(error) => voidNej--Kaldes ved fejl. Fejlen har felterne error (kode) og message.
ringtoneboolean | stringNejfalseAfspil en ringetone under forbindelsen. true for standardringetonen eller en URL-streng til brugerdefineret lyd.

Returværdi

Hooket returnerer et UseThunderPhoneReturn-objekt:

EgenskabTypeBeskrivelse
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Aktuel forbindelsestilstand.
connect() => voidStart en stemmesession.
disconnect() => voidAfslut den aktuelle session.
toggleMute() => voidSlå mikrofonens mute til eller fra.
isMutedbooleanOm mikrofonen aktuelt er muted.
errorstring | undefinedFejlmeddelelse, når tilstanden er 'error'.
agentNamestring | undefinedVist navn på den tilsluttede agent.
audioLevelnumberForældet -- altid 0. En statisk pladsholder, der bevares for bagudkompatibilitet; den opdateres aldrig. Læs audioLevelRef.current i stedet.
audioLevelRefReact.RefObject<number>En muterbar ref, der indeholder lydniveauet i realtid (0--1) -- det højeste af agentens stemme og den besøgendes mikrofon -- opdateret på hvert animationsframe uden for Reacts renderingscyklus. Læs audioLevelRef.current i requestAnimationFrame-løkker for jævne animationer uden hakken, eller aflæs den med et interval, når du har brug for værdien i React-state.
audioReactNodeUsynligt element, der håndterer lydforbindelsen -- skal renderes.

Lydreaktivt brugerinterface

Ref'en audioLevelRef giver dig lydniveauer ved billedfrekvens uden at udloese React-genrenderinger, hvilket goer den ideel til at styre glatte boelgeformsvisualiseringer, pulserende kugler eller enhver animation, der er knyttet til samtalen. Niveauet afspejler den lyd, der er hoejest: agentens stemme eller besoegendes mikrofon.

Eksempel paa boelgeform

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 paa pulserende kugle

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 paa taleindikator

For React-gengivet brugerinterface, der aendrer sig med lydstyrken -- som et maerkat for "taler" baseret paa en taerskelvaerdi -- skal du maale audioLevelRef.current med et interval og gemme 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>
  )
}

Tilstandsmaskine

Egenskaben state følger denne livscyklus:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
TilstandBeskrivelse
idleIngen aktiv session. Klar til at kalde connect().
connectingSessionen oprettes. Deaktiver opkaldsknappen i denne tilstand.
connectedStemmesessionen er aktiv. Brugeren taler med agenten.
disconnectedSessionen er afsluttet korrekt. Skifter automatisk tilbage til idle efter 1,5 sekunder.
errorNoget gik galt. Se phone.error for meddelelsen. Tilstanden ryddes ikke af sig selv -- når du kalder connect() igen, starter det et nyt forsøg og nulstiller fejlen.

Eksempler

Med lydstyring

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

Afspil en ringelyd under forbindelsen for at simulere et telefonopkald:

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 afspilles i sløjfe under tilstanden connecting og toner ud, når agenten forbindes. Angiv true for den indbyggede standardringetone eller en URL-streng for at bruge din egen lydfil.

Med hændelseskald

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

Fuldt tilpasset brugerflade

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

Elementet phone.audio er usynligt, men påkrævet. Placer det hvor som helst i din JSX -- det renderer ingen synlig DOM, men administrerer WebRTC-lydforbindelsen internt.

Deaktiver knappen under tilslutning

Tilstanden connecting kan vare 1-3 sekunder. Deaktiver opkaldsknappen i denne tilstand for at forhindre dublerede forbindelsesforsøg.

Håndter fejltilstanden elegant

Når tilstanden er error, skal du vise phone.error til brugeren og holde opkaldsknappen aktiveret. Hooken forlader ikke selv tilstanden error -- et nyt kald til connect() starter et nyt forsøg og rydder den forrige fejl.

Brug callbacks til sideeffekter

Callbacksene onConnect, onDisconnect og onError er ideelle til analyse, logføring eller til at udløse anden applikationslogik uden at polle tilstanden.

Læs lydniveauer fra audioLevelRef

audioLevelRef er den eneste kilde til live-lydniveauer. Læs audioLevelRef.current inde i requestAnimationFrame for jævne animationer som bølgeformer (at læse en ref medfører ikke genrenderinger), eller udtag prøver med et interval og gem resultatet i state for React-renderet brugergrænseflade. Tallet audioLevel er forældet og altid 0 -- byg ikke logik på det.