---
title: "Headless Hook"
description: "useThunderPhone React হুক দিয়ে সম্পূর্ণ কাস্টম ভয়েস UI তৈরি করুন"
---

`useThunderPhone` হুক আপনাকে ইউজার ইন্টারফেসের ওপর সম্পূর্ণ নিয়ন্ত্রণ দেয়, আর ThunderPhone ভয়েস সেশন, অডিও রাউটিং এবং সংযোগের অবস্থা পরিচালনা করে। যখন আপনি সম্পূর্ণ কাস্টম UI চান -- নিজের বাটন, লেআউট, অ্যানিমেশন এবং ব্র্যান্ডিংসহ -- এবং ThunderPhone ভেতরের সবকিছু পরিচালনা করবে, তখন এটি ব্যবহার করুন।

## কখন হেডলেস হুক ব্যবহার করবেন

পূর্বনির্মিত `ThunderPhoneWidget` কম্পোনেন্ট অধিকাংশ ব্যবহারের ক্ষেত্র পূরণ করে, তবে নিচের প্রয়োজন হলে হেডলেস হুক ব্যবহার করুন:

- আপনার অ্যাপের ডিজাইন সিস্টেমের সঙ্গে মেলে এমন সম্পূর্ণ কাস্টম কল UI
- রিয়েল-টাইম অডিও লেভেল দ্বারা চালিত অডিও-প্রতিক্রিয়াশীল ভিজ্যুয়ালাইজেশন (ওয়েভফর্ম, অর্ব, স্পন্দিত নির্দেশক)
- কাস্টম কল ফ্লো, যেমন কল-পূর্ব ফর্ম, কল-পরবর্তী জরিপ, বা ভয়েসের পাশাপাশি ইনলাইন চ্যাট
- বিদ্যমান কম্পোনেন্ট লাইব্রেরির সঙ্গে ইন্টিগ্রেশন (Material UI, Chakra, Radix ইত্যাদি)

---

## ইনস্টলেশন

```bash
npm install @thunderphone/widget
```

<Note>
  হেডলেস হুকের জন্য `@thunderphone/widget/style.css` ইমপোর্ট করার প্রয়োজন **নেই**, কারণ আপনি নিজের UI দিচ্ছেন। তবে আপনাকে একই `@thunderphone/widget` প্যাকেজ ইনস্টল করতেই হবে।
</Note>

---

## মৌলিক ব্যবহার

```tsx
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>

---

## অপশন

`UseThunderPhoneOptions` এর মাধ্যমে `useThunderPhone`-এ এই অপশনগুলো পাস করুন:

| অপশন | ধরন | প্রয়োজনীয় | ডিফল্ট | বিবরণ |
|--------|------|----------|---------|-------------|
| `publishableKey` | `string` | হ্যাঁ | -- | প্রকাশযোগ্য API কী (`pk_live_...`)। কী-এর উইজেট কনফিগারেশন থেকে এজেন্ট স্বয়ংক্রিয়ভাবে নির্ধারিত হয়। |
| `apiBase` | `string` | না | `'https://api.thunderphone.com/v1'` | API বেস URL ওভাররাইড। |
| `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) ধারণকারী একটি পরিবর্তনযোগ্য ref -- এজেন্টের ভয়েস এবং ভিজিটরের মাইক্রোফোনের মধ্যে যেটি বেশি জোরে -- যা প্রতিটি অ্যানিমেশন ফ্রেমে React-এর রেন্ডার সাইকেলের বাইরে আপডেট হয়। মসৃণ, জ্যাঙ্ক-মুক্ত অ্যানিমেশনের জন্য `requestAnimationFrame` লুপের মধ্যে `audioLevelRef.current` পড়ুন, অথবা React স্টেটে ভ্যালু প্রয়োজন হলে নির্দিষ্ট ইন্টারভ্যালে এটি স্যাম্পল করুন। |
| `audio` | `ReactNode` | অডিও সংযোগ পরিচালনাকারী অদৃশ্য এলিমেন্ট -- **অবশ্যই রেন্ডার করতে হবে**। |

---

## অডিও-প্রতিক্রিয়াশীল UI

`audioLevelRef` ref আপনাকে React re-render ট্রিগার না করেই ফ্রেম-রেট অডিও লেভেল দেয়, ফলে এটি মসৃণ waveform ভিজ্যুয়ালাইজেশন, স্পন্দিত orb বা কথোপকথনের সঙ্গে যুক্ত যেকোনো অ্যানিমেশন চালানোর জন্য আদর্শ। লেভেলটি এজেন্টের ভয়েস বা ভিজিটরের মাইক্রোফোনের মধ্যে যেটি বেশি জোরে শোনা যায়, সেটিই প্রতিফলিত করে।

### Waveform উদাহরণ

```tsx
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>
  )
}
```

### স্পন্দিত Orb উদাহরণ

```tsx
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-rendered UI-এর জন্য -- যেমন থ্রেশহোল্ড-ভিত্তিক "কথা বলছে" ব্যাজ -- নির্দিষ্ট বিরতিতে `audioLevelRef.current` নমুনা নিন এবং ফলাফল state-এ সংরক্ষণ করুন:

```tsx
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` থেকে লেভেল পড়ুন। return object-এ থাকা `audioLevel` সংখ্যাটি **অপ্রচলিত এবং সর্বদা `0`** -- এর ওপর ভিত্তি করে তৈরি যেকোনো লজিক নীরবে শূন্য পড়বে।
</Warning>

---

## স্টেট মেশিন

`state` প্রপার্টি এই লাইফসাইকেল অনুসরণ করে:

```
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
```

| স্টেট | বিবরণ |
|-------|-------------|
| `idle` | কোনো সক্রিয় সেশন নেই। `connect()` কল করার জন্য প্রস্তুত। |
| `connecting` | সেশন স্থাপন করা হচ্ছে। এই স্টেটে কল বাটন নিষ্ক্রিয় করুন। |
| `connected` | ভয়েস সেশন সক্রিয়। ব্যবহারকারী এজেন্টের সঙ্গে কথা বলছেন। |
| `disconnected` | সেশন সঠিকভাবে শেষ হয়েছে। 1.5 সেকেন্ড পর স্বয়ংক্রিয়ভাবে আবার `idle`-এ চলে যায়। |
| `error` | কিছু ভুল হয়েছে। বার্তার জন্য `phone.error` দেখুন। স্টেটটি নিজে থেকে **ক্লিয়ার** হয় না -- আবার `connect()` কল করলে একটি নতুন চেষ্টা শুরু হয় এবং ত্রুটিটি রিসেট হয়। |

---

## উদাহরণ

### মিউট নিয়ন্ত্রণসহ

```tsx
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
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
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}
    </>
  )
}
```

### সম্পূর্ণ কাস্টম UI

```tsx
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` একমাত্র লাইভ অডিও-লেভেল উৎস। ওয়েভফর্মের মতো মসৃণ অ্যানিমেশনের জন্য `requestAnimationFrame`-এর মধ্যে `audioLevelRef.current` পড়ুন (একটি ref পড়লে পুনরায় রেন্ডার হয় না), অথবা নির্দিষ্ট বিরতিতে এটি স্যাম্পল করে ফলাফল স্টেটে সংরক্ষণ করুন React-রেন্ডার করা UI-এর জন্য। `audioLevel` সংখ্যাটি অবচিত এবং সবসময় `0` -- এর ওপর কোনো লজিক তৈরি করবেন না।
  </Accordion>
</AccordionGroup>
