Headless Hook

Το hook useThunderPhone σάς δίνει πλήρη έλεγχο του περιβάλλοντος χρήστη, ενώ το ThunderPhone διαχειρίζεται τη φωνητική συνεδρία, τη δρομολόγηση ήχου και την κατάσταση σύνδεσης. Χρησιμοποιήστε το όταν θέλετε ένα πλήρως προσαρμοσμένο UI -- με τα δικά σας κουμπιά, διατάξεις, κινούμενα εφέ και επωνυμία -- ενώ το ThunderPhone αναλαμβάνει όλα τα υπόλοιπα στο παρασκήνιο.

Πότε να χρησιμοποιήσετε το Headless Hook

Το έτοιμο component ThunderPhoneWidget καλύπτει τις περισσότερες περιπτώσεις χρήσης, αλλά επιλέξτε το headless hook όταν χρειάζεστε:


Εγκατάσταση

npm install @thunderphone/widget

Βασική χρήση

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

Επιλογές

Περάστε αυτές τις επιλογές στο useThunderPhone μέσω του UseThunderPhoneOptions:

ΕπιλογήΤύποςΥποχρεωτικόΠροεπιλογήΠεριγραφή
publishableKeystringΝαι--Δημοσιεύσιμο κλειδί API (pk_live_...). Ο πράκτορας προσδιορίζεται αυτόματα από τη διαμόρφωση widget του κλειδιού.
apiBasestringΌχι'https://api.thunderphone.com/v1'Παράκαμψη του βασικού URL του API.
languagestringΌχι--Παράκαμψη γλώσσας ανά συνεδρία -- ένας κωδικός γλώσσας ή τοπικές ρυθμίσεις όπως en, es ή fr-FR. Όταν δεν ορίζεται, εφαρμόζεται η διαμορφωμένη γλώσσα του πράκτορα.
voicestringΌχι--Παράκαμψη φωνής ανά συνεδρία -- ένα όνομα φωνής όπως maria. Όταν δεν ορίζεται, εφαρμόζεται η διαμορφωμένη φωνή του πράκτορα.
contextstringΌχι--Πραγματολογικό περιεχόμενο σελίδας ή ιστοτόπου ανά συνεδρία που μεταβιβάζεται στον πράκτορα. Περικόπτεται στην πλευρά του διακομιστή σε 12.000 χαρακτήρες.
onConnect() => voidΌχι--Καλείται όταν συνδέεται η φωνητική συνεδρία.
onDisconnect() => voidΌχι--Καλείται όταν λήγει η συνεδρία.
onError(error) => voidΌχι--Καλείται σε σφάλματα. Το σφάλμα διαθέτει τα πεδία error (κωδικός) και message.
ringtoneboolean | stringΌχιfalseΑναπαραγωγή ήχου κλήσης κατά τη σύνδεση. true για τον προεπιλεγμένο ήχο κλήσης ή συμβολοσειρά URL για προσαρμοσμένο ήχο.

Τιμή επιστροφής

Το hook επιστρέφει ένα αντικείμενο UseThunderPhoneReturn:

ΙδιότηταΤύποςΠεριγραφή
state'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'Τρέχουσα κατάσταση σύνδεσης.
connect() => voidΈναρξη φωνητικής συνεδρίας.
disconnect() => voidΤερματισμός της τρέχουσας συνεδρίας.
toggleMute() => voidΕναλλαγή σίγασης/κατάργησης σίγασης μικροφώνου.
isMutedbooleanΑν το μικρόφωνο είναι αυτήν τη στιγμή σε σίγαση.
errorstring | undefinedΜήνυμα σφάλματος όταν η κατάσταση είναι 'error'.
agentNamestring | undefinedΕμφανιζόμενο όνομα του συνδεδεμένου πράκτορα.
audioLevelnumberΚαταργημένο -- πάντα 0. Ένα στατικό σύμβολο κράτησης που διατηρείται για συμβατότητα με προηγούμενες εκδόσεις· δεν ενημερώνεται ποτέ. Διαβάστε αντ' αυτού το audioLevelRef.current.
audioLevelRefReact.RefObject<number>Μια μεταβλητή αναφορά που περιέχει το επίπεδο ήχου σε πραγματικό χρόνο (0--1) -- το υψηλότερο μεταξύ της φωνής του πράκτορα και του μικροφώνου του επισκέπτη -- και ενημερώνεται σε κάθε καρέ κίνησης, εκτός του κύκλου απόδοσης του React. Διαβάστε το audioLevelRef.current μέσα σε βρόχους requestAnimationFrame για ομαλές κινήσεις χωρίς κολλήματα ή δειγματοληπτήστε το σε ένα χρονικό διάστημα όταν χρειάζεστε την τιμή στην κατάσταση του React.
audioReactNodeΑόρατο στοιχείο που διαχειρίζεται τη σύνδεση ήχου -- πρέπει να αποδίδεται.

Διεπαφή χρήστη που αντιδρά στον ήχο

Το ref audioLevelRef σάς παρέχει επίπεδα ήχου με ρυθμό καρέ χωρίς να ενεργοποιεί επαναποδόσεις του React, καθιστώντας το ιδανικό για ομαλές οπτικοποιήσεις κυματομορφής, παλλόμενες σφαίρες ή οποιαδήποτε κινούμενη εικόνα συνδέεται με τη συνομιλία. Το επίπεδο αντικατοπτρίζει όποιο είναι πιο δυνατό: η φωνή του πράκτορα ή το μικρόφωνο του επισκέπτη.

Παράδειγμα κυματομορφής

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

Παράδειγμα παλλόμενης σφαίρας

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

Παράδειγμα ένδειξης ομιλίας

Για διεπαφή χρήστη που αποδίδεται από το React και αλλάζει ανάλογα με την ένταση -- όπως μια ένδειξη «ομιλίας» βάσει ορίου -- δειγματοληπτείτε το audioLevelRef.current σε ένα διάστημα και αποθηκεύστε το αποτέλεσμα στην κατάσταση:

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

Μηχανή καταστάσεων

Η ιδιότητα state ακολουθεί αυτόν τον κύκλο ζωής:

idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
ΚατάστασηΠεριγραφή
idleΔεν υπάρχει ενεργή συνεδρία. Έτοιμο για κλήση του connect().
connectingΗ συνεδρία δημιουργείται. Απενεργοποιήστε το κουμπί κλήσης σε αυτήν την κατάσταση.
connectedΗ φωνητική συνεδρία είναι ενεργή. Ο χρήστης συνομιλεί με τον πράκτορα.
disconnectedΗ συνεδρία ολοκληρώθηκε κανονικά. Μεταβαίνει αυτόματα ξανά σε idle μετά από 1.5 δευτερόλεπτα.
errorΚάτι πήγε στραβά. Ελέγξτε το phone.error για το μήνυμα. Η κατάσταση δεν εκκαθαρίζεται από μόνη της -- η εκ νέου κλήση του connect() ξεκινά μια νέα προσπάθεια και επαναφέρει το σφάλμα.

Παραδείγματα

Με έλεγχο σίγασης

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

Με ήχο κλήσης

Αναπαραγάγετε έναν ήχο κλήσης κατά τη σύνδεση για να προσομοιώσετε μια τηλεφωνική κλήση:

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

Ο ήχος κλήσης επαναλαμβάνεται κατά την κατάσταση connecting και χαμηλώνει σταδιακά όταν συνδέεται ο πράκτορας. Περάστε true για τον ενσωματωμένο προεπιλεγμένο ήχο κλήσης ή μια συμβολοσειρά URL για να χρησιμοποιήσετε το δικό σας αρχείο ήχου.

Με επανακλήσεις συμβάντων

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

Πλήρως προσαρμοσμένο περιβάλλον εργασίας χρήστη

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

Συμβουλές

Να αποδίδετε πάντα το phone.audio

Το στοιχείο phone.audio είναι αόρατο αλλά απαραίτητο. Τοποθετήστε το οπουδήποτε στο JSX σας -- δεν αποδίδει ορατό DOM, αλλά διαχειρίζεται εσωτερικά τη σύνδεση ήχου WebRTC.

Απενεργοποιήστε το κουμπί κατά τη σύνδεση

Η κατάσταση connecting μπορεί να διαρκέσει 1-3 δευτερόλεπτα. Απενεργοποιήστε το κουμπί κλήσης κατά τη διάρκεια αυτής της κατάστασης, ώστε να αποτρέψετε διπλές προσπάθειες σύνδεσης.

Χειριστείτε ομαλά την κατάσταση σφάλματος

Όταν η κατάσταση είναι error, εμφανίστε το phone.error στον χρήστη και διατηρήστε ενεργοποιημένο το κουμπί κλήσης. Το hook δεν εξέρχεται από την κατάσταση error από μόνο του -- η εκ νέου κλήση του connect() ξεκινά μια νέα προσπάθεια και διαγράφει το προηγούμενο σφάλμα.

Χρησιμοποιήστε callbacks για παρενέργειες

Τα callbacks onConnect, onDisconnect και onError είναι ιδανικά για αναλυτικά στοιχεία, καταγραφή ή ενεργοποίηση άλλης λογικής εφαρμογής χωρίς έλεγχο της κατάστασης με polling.

Διαβάστε τα επίπεδα ήχου από το audioLevelRef

Το audioLevelRef είναι η μοναδική ζωντανή πηγή επιπέδου ήχου. Διαβάστε το audioLevelRef.current μέσα στο requestAnimationFrame για ομαλά κινούμενα γραφικά όπως κυματομορφές (η ανάγνωση ενός ref δεν προκαλεί επαναποδόσεις), ή δειγματοληπτήστε το σε ένα χρονικό διάστημα και αποθηκεύστε το αποτέλεσμα στην κατάσταση για UI που αποδίδεται από React. Ο αριθμός audioLevel έχει καταργηθεί και είναι πάντα 0 -- μην βασίζετε λογική σε αυτόν.