ThunderPhone 2.0 متاح الآن.خدمة ذاتية، ابتداءً من 2 سنت/دقيقة.اقرأ الإعلان

Widget

خطاف Headless

أنشئ واجهة صوتية مخصصة بالكامل باستخدام خطاف React useThunderPhone

يمنحك الخطاف 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 لصوت مخصص.

قيمة الإرجاع

يعيد الخطاف كائن 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عنصر غير مرئي يتعامل مع اتصال الصوت -- يجب عرضه.

واجهة مستخدم متفاعلة مع الصوت

يمنحك المرجع 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 للمستخدم وأبقِ زر الاتصال مفعّلًا. لا تخرج الخطّافة من حالة error تلقائيًا — إذ يؤدي استدعاء connect() مرة أخرى إلى بدء محاولة جديدة ومسح الخطأ السابق.

استخدم ردود النداء للتأثيرات الجانبية

تُعد ردود النداء onConnect وonDisconnect وonError مثالية للتحليلات أو التسجيل أو تشغيل منطق آخر في التطبيق دون استطلاع الحالة.

اقرأ مستويات الصوت من audioLevelRef

يُعد audioLevelRef المصدر الوحيد المباشر لمستوى الصوت. اقرأ audioLevelRef.current داخل requestAnimationFrame للحصول على رسوم متحركة سلسة، مثل الأشكال الموجية (قراءة المرجع لا تؤدي إلى إعادة العرض)، أو خذ عينة منه على فواصل زمنية وخزّن النتيجة في الحالة لواجهة مستخدم يعرضها React. الرقم audioLevel مهمل وتكون قيمته دائمًا 0 — لا تبنِ عليه أي منطق.