Headless Hook
Το hook useThunderPhone σάς δίνει πλήρη έλεγχο του περιβάλλοντος χρήστη, ενώ το ThunderPhone διαχειρίζεται τη φωνητική συνεδρία, τη δρομολόγηση ήχου και την κατάσταση σύνδεσης. Χρησιμοποιήστε το όταν θέλετε ένα πλήρως προσαρμοσμένο UI -- με τα δικά σας κουμπιά, διατάξεις, κινούμενα εφέ και επωνυμία -- ενώ το ThunderPhone αναλαμβάνει όλα τα υπόλοιπα στο παρασκήνιο.
Πότε να χρησιμοποιήσετε το Headless Hook
Το έτοιμο component ThunderPhoneWidget καλύπτει τις περισσότερες περιπτώσεις χρήσης, αλλά επιλέξτε το headless hook όταν χρειάζεστε:
- Ένα πλήρως προσαρμοσμένο UI κλήσης που ταιριάζει με το σχεδιαστικό σύστημα της εφαρμογής σας
- Οπτικοποιήσεις που αντιδρούν στον ήχο (κυματομορφές, σφαίρες, παλλόμενες ενδείξεις) βάσει επιπέδων ήχου σε πραγματικό χρόνο
- Προσαρμοσμένες ροές κλήσεων, όπως φόρμες πριν από την κλήση, έρευνες μετά την κλήση ή ενσωματωμένη συνομιλία παράλληλα με τη φωνή
- Ενσωμάτωση σε μια υπάρχουσα βιβλιοθήκη components (Material UI, Chakra, Radix κ.λπ.)
Εγκατάσταση
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:
| Επιλογή | Τύπος | Υποχρεωτικό | Προεπιλογή | Περιγραφή |
|---|---|---|---|---|
publishableKey | string | Ναι | -- | Δημοσιεύσιμο κλειδί API (pk_live_...). Ο πράκτορας προσδιορίζεται αυτόματα από τη διαμόρφωση widget του κλειδιού. |
apiBase | string | Όχι | 'https://api.thunderphone.com/v1' | Παράκαμψη του βασικού URL του API. |
language | string | Όχι | -- | Παράκαμψη γλώσσας ανά συνεδρία -- ένας κωδικός γλώσσας ή τοπικές ρυθμίσεις όπως en, es ή fr-FR. Όταν δεν ορίζεται, εφαρμόζεται η διαμορφωμένη γλώσσα του πράκτορα. |
voice | string | Όχι | -- | Παράκαμψη φωνής ανά συνεδρία -- ένα όνομα φωνής όπως maria. Όταν δεν ορίζεται, εφαρμόζεται η διαμορφωμένη φωνή του πράκτορα. |
context | string | Όχι | -- | Πραγματολογικό περιεχόμενο σελίδας ή ιστοτόπου ανά συνεδρία που μεταβιβάζεται στον πράκτορα. Περικόπτεται στην πλευρά του διακομιστή σε 12.000 χαρακτήρες. |
onConnect | () => void | Όχι | -- | Καλείται όταν συνδέεται η φωνητική συνεδρία. |
onDisconnect | () => void | Όχι | -- | Καλείται όταν λήγει η συνεδρία. |
onError | (error) => void | Όχι | -- | Καλείται σε σφάλματα. Το σφάλμα διαθέτει τα πεδία error (κωδικός) και message. |
ringtone | boolean | string | Όχι | false | Αναπαραγωγή ήχου κλήσης κατά τη σύνδεση. true για τον προεπιλεγμένο ήχο κλήσης ή συμβολοσειρά URL για προσαρμοσμένο ήχο. |
Τιμή επιστροφής
Το hook επιστρέφει ένα αντικείμενο UseThunderPhoneReturn:
| Ιδιότητα | Τύπος | Περιγραφή |
|---|---|---|
state | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Τρέχουσα κατάσταση σύνδεσης. |
connect | () => void | Έναρξη φωνητικής συνεδρίας. |
disconnect | () => void | Τερματισμός της τρέχουσας συνεδρίας. |
toggleMute | () => void | Εναλλαγή σίγασης/κατάργησης σίγασης μικροφώνου. |
isMuted | boolean | Αν το μικρόφωνο είναι αυτήν τη στιγμή σε σίγαση. |
error | string | undefined | Μήνυμα σφάλματος όταν η κατάσταση είναι 'error'. |
agentName | string | undefined | Εμφανιζόμενο όνομα του συνδεδεμένου πράκτορα. |
audioLevel | number | Καταργημένο -- πάντα 0. Ένα στατικό σύμβολο κράτησης που διατηρείται για συμβατότητα με προηγούμενες εκδόσεις· δεν ενημερώνεται ποτέ. Διαβάστε αντ' αυτού το audioLevelRef.current. |
audioLevelRef | React.RefObject<number> | Μια μεταβλητή αναφορά που περιέχει το επίπεδο ήχου σε πραγματικό χρόνο (0--1) -- το υψηλότερο μεταξύ της φωνής του πράκτορα και του μικροφώνου του επισκέπτη -- και ενημερώνεται σε κάθε καρέ κίνησης, εκτός του κύκλου απόδοσης του React. Διαβάστε το audioLevelRef.current μέσα σε βρόχους requestAnimationFrame για ομαλές κινήσεις χωρίς κολλήματα ή δειγματοληπτήστε το σε ένα χρονικό διάστημα όταν χρειάζεστε την τιμή στην κατάσταση του React. |
audio | ReactNode | Αόρατο στοιχείο που διαχειρίζεται τη σύνδεση ήχου -- πρέπει να αποδίδεται. |
Διεπαφή χρήστη που αντιδρά στον ήχο
Το 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 -- μην βασίζετε λογική σε αυτόν.