> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# خطاف Headless

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

يمنحك الخطاف `useThunderPhone` تحكمًا كاملًا في واجهة المستخدم بينما يدير ThunderPhone جلسة الصوت وتوجيه الصوت وحالة الاتصال. استخدمه عندما تريد واجهة مستخدم مخصصة بالكامل — بأزرارك وتخطيطاتك ورسومك المتحركة وهويتك التجارية — بينما يتولى ThunderPhone كل شيء في الخلفية.

## متى تستخدم الخطاف بلا واجهة

يغطي المكوّن `ThunderPhoneWidget` المُنشأ مسبقًا معظم حالات الاستخدام، لكن استخدم الخطاف بلا واجهة عندما تحتاج إلى:

* واجهة مكالمات مخصصة بالكامل تتوافق مع منظومة تصميم تطبيقك
* تصورات مرئية تتفاعل مع الصوت (أشكال موجية أو كرات أو مؤشرات نابضة) مدفوعة بمستويات الصوت في الوقت الفعلي
* تدفقات مكالمات مخصصة، مثل نماذج ما قبل المكالمة واستبيانات ما بعد المكالمة أو دردشة مضمّنة بجانب الصوت
* التكامل مع مكتبة مكوّنات موجودة (Material UI أو Chakra أو Radix وغيرها)

***

## التثبيت

```bash theme={null}
npm install @thunderphone/widget
```

<Note>
  لا يتطلب الخطاف بلا واجهة استيراد `@thunderphone/widget/style.css` لأنك توفر واجهة المستخدم الخاصة بك. ومع ذلك، يجب عليك تثبيت حزمة `@thunderphone/widget` نفسها.
</Note>

***

## الاستخدام الأساسي

```tsx theme={null}
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}
    </>
  )
}
```

<Warning>
  **يجب عرض `phone.audio` في مكان ما ضمن شجرة مكوّناتك.** فهو عنصر React غير مرئي يدير اتصال الصوت الأساسي. إذا حذفته، فلن يُشغَّل أي صوت ولن تعمل الجلسة.
</Warning>

***

## الخيارات

مرّر هذه الخيارات إلى `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 لصوت مخصص.                               |

<Note>
  الخطاف بلا واجهة: فهو **لا** يقبل خصائص مظهر `ThunderPhoneWidget` (`theme` و`primaryColor` و`title` و`position` و`className`). يؤدي تمريرها إلى خطأ TypeScript — فالعرض التقديمي متروك لك بالكامل لإنشائه.
</Note>

***

## قيمة الإرجاع

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

***

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

يمنحك المرجع `audioLevelRef` مستويات الصوت بمعدل الإطارات دون تشغيل عمليات إعادة العرض في React، ما يجعله مثاليًا لتشغيل تصورات موجية سلسة، أو كرات نابضة، أو أي حركة مرتبطة بالمحادثة. يعكس المستوى أيهما أعلى صوتًا: صوت الوكيل أو ميكروفون الزائر.

### مثال على الشكل الموجي

```tsx theme={null}
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>
  )
}
```

### مثال على الكرة النابضة

```tsx theme={null}
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` على فترات وخزّن النتيجة في الحالة:

```tsx theme={null}
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>
  )
}
```

<Warning>
  اقرأ المستويات دائمًا من `audioLevelRef.current`. رقم `audioLevel` في كائن الإرجاع **متوقف الدعم عنه ويكون دائمًا `0`** -- أي منطق مبني عليه سيقرأ الصفر بصمت.
</Warning>

***

## آلة الحالات

تتبع الخاصية `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()` مجددًا إلى بدء محاولة جديدة وإعادة تعيين الخطأ. |

***

## أمثلة

### مع التحكم في كتم الصوت

```tsx theme={null}
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>
  )
}
```

### مع نغمة الرنين

شغّل صوت رنين أثناء الاتصال لمحاكاة مكالمة هاتفية:

```tsx theme={null}
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 لاستخدام ملفك الصوتي الخاص.

### مع استدعاءات الأحداث

```tsx theme={null}
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}
    </>
  )
}
```

### واجهة مستخدم مخصصة بالكامل

```tsx theme={null}
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>
  )
}
```

***

## نصائح

<AccordionGroup>
  <Accordion title="اعرض phone.audio دائمًا">
    العنصر `phone.audio` غير مرئي لكنه مطلوب. ضعه في أي مكان داخل JSX — فهو لا يعرض أي DOM مرئي، لكنه يدير اتصال الصوت عبر WebRTC داخليًا.
  </Accordion>

  <Accordion title="عطّل الزر أثناء الاتصال">
    قد تستمر حالة `connecting` لمدة 1-3 ثوانٍ. عطّل زر الاتصال خلال هذه الحالة لمنع محاولات الاتصال المكررة.
  </Accordion>

  <Accordion title="تعامل مع حالة الخطأ بسلاسة">
    عندما تكون الحالة `error`، اعرض `phone.error` للمستخدم وأبقِ زر الاتصال مفعّلًا. لا تخرج الخطّافة من حالة `error` تلقائيًا — إذ يؤدي استدعاء `connect()` مرة أخرى إلى بدء محاولة جديدة ومسح الخطأ السابق.
  </Accordion>

  <Accordion title="استخدم ردود النداء للتأثيرات الجانبية">
    تُعد ردود النداء `onConnect` و`onDisconnect` و`onError` مثالية للتحليلات أو التسجيل أو تشغيل منطق آخر في التطبيق دون استطلاع الحالة.
  </Accordion>

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