ThunderPhone 2.0 באוויר.בשירות עצמי, החל מ-2¢/דקה.קראו את הודעת ההשקה

Widget

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:

אפשרותסוגנדרשברירת מחדלתיאור
publishableKeystringכן--מפתח API ניתן לפרסום (pk_live_...). הסוכן מזוהה אוטומטית מהגדרת הווידג'ט של המפתח.
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 מתאימים במיוחד לאנליטיקה, לרישום לוגים או להפעלת לוגיקה אחרת באפליקציה ללא תשאול של המצב.

קראו רמות שמע מ-audioLevelRef

audioLevelRef הוא המקור החי היחיד לרמת שמע. קראו את audioLevelRef.current בתוך requestAnimationFrame עבור הנפשות חלקות כמו צורות גל (קריאה מ-ref אינה גורמת לעיבודים מחדש), או דגמו אותו במרווחי זמן ושמרו את התוצאה במצב עבור ממשק משתמש שמרונדר על ידי React. המספר audioLevel הוצא משימוש ותמיד הוא 0 -- אל תבנו עליו לוגיקה.