---
title: "Headless Hook"
description: "Направите потпуно прилагођен гласовни кориснички интерфејс помоћу React куке useThunderPhone"
---

Кука `useThunderPhone` Вам пружа потпуну контролу над корисничким интерфејсом док ThunderPhone управља гласовном сесијом, усмеравањем звука и стањем везе. Користите је када желите потпуно прилагођен кориснички интерфејс -- сопствене дугмад, распореде, анимације и брендирање -- док ThunderPhone обавља све у позадини.

## Када користити куку без интерфејса

Унапред изграђена компонента `ThunderPhoneWidget` покрива већину случајева употребе, али користите куку без интерфејса када Вам је потребно:

- Потпуно прилагођен кориснички интерфејс за позиве који одговара систему дизајна Ваше апликације
- Визуелизације које реагују на звук (таласни облици, сфере, пулсирајући индикатори) покретане нивоима звука у реалном времену
- Прилагођени токови позива, као што су обрасци пре позива, анкете након позива или уграђени ћаскање уз глас
- Интеграција у постојећу библиотеку компоненти (Material UI, Chakra, Radix итд.)

---

## Инсталација

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

<Note>
  Кука без интерфејса **не** захтева увоз `@thunderphone/widget/style.css` јер обезбеђујете сопствени кориснички интерфејс. Међутим, и даље морате инсталирати исти пакет `@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>

---

## Опције

Проследите ове опције у `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
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
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
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
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}
    </>
  )
}
```

### Потпуно прилагођени кориснички интерфејс

```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` и задржите омогућено дугме за позив. Hook не напушта самостално стање `error` -- поновни позив `connect()` покреће нови покушај и брише претходну грешку.
  </Accordion>

  <Accordion title="Користите повратне функције за споредне ефекте">
    Повратне функције `onConnect`, `onDisconnect` и `onError` идеалне су за аналитику, евидентирање или покретање друге логике апликације без провере стања у интервалима.
  </Accordion>

  <Accordion title="Читајте нивое звука из audioLevelRef">
    `audioLevelRef` је једини извор нивоа звука уживо. Читајте `audioLevelRef.current` унутар `requestAnimationFrame` за глатке анимације као што су таласни облици (читање референце не изазива поновно рендеровање) или га узоркујте у интервалима и сачувајте резултат у стању за UI који рендерује React. Број `audioLevel` је застарео и увек је `0` -- немојте градити логику на њему.
  </Accordion>
</AccordionGroup>
