Headless Hook
בנו ממשק משתמש קולי מותאם אישית במלואו באמצעות ה-Hook useThunderPhone של React
ההוק useThunderPhone מעניק לכם שליטה מלאה בממשק המשתמש, בעוד ThunderPhone מנהל את סשן הקול, ניתוב השמע ומצב החיבור. השתמשו בו כאשר אתם רוצים ממשק משתמש מותאם אישית לחלוטין -- עם הכפתורים, הפריסות, ההנפשות והמיתוג שלכם -- בעוד ThunderPhone מטפל בכל מה שמאחורי הקלעים.
מתי להשתמש בהוק ללא ממשק
הרכיב המובנה ThunderPhoneWidget מתאים לרוב תרחישי השימוש, אך השתמשו בהוק ללא ממשק כאשר אתם זקוקים ל:
- ממשק שיחה מותאם אישית לחלוטין שתואם למערכת העיצוב של האפליקציה שלכם
- המחשות חזותיות המגיבות לשמע (צורות גל, כדורים, מחוונים פועמים) המונעות על ידי רמות שמע בזמן אמת
- תהליכי שיחה מותאמים אישית, כגון טפסים לפני שיחה, סקרים לאחר שיחה או צ'אט מוטמע לצד קול
- שילוב בספריית רכיבים קיימת (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_...). הסוכן מזוהה אוטומטית מהגדרת הווידג'ט של המפתח. |
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 מתאימים במיוחד לאנליטיקה, לרישום לוגים או להפעלת לוגיקה אחרת באפליקציה ללא תשאול של המצב.
קראו רמות שמע מ-audioLevelRef
audioLevelRef הוא המקור החי היחיד לרמת שמע. קראו את audioLevelRef.current בתוך requestAnimationFrame עבור הנפשות חלקות כמו צורות גל (קריאה מ-ref אינה גורמת לעיבודים מחדש), או דגמו אותו במרווחי זמן ושמרו את התוצאה במצב עבור ממשק משתמש שמרונדר על ידי React. המספר audioLevel הוצא משימוש ותמיד הוא 0 -- אל תבנו עליו לוגיקה.