---
title: "Headless Hook"
description: "Sukurkite visiškai pritaikytą balso sąsają naudodami „useThunderPhone“ React kablį"
---

`useThunderPhone` kabliukas suteikia jums visišką naudotojo sąsajos valdymą, o ThunderPhone tvarko balso sesiją, garso nukreipimą ir ryšio būseną. Naudokite jį, kai norite visiškai pritaikytos naudotojo sąsajos -- savo mygtukų, išdėstymų, animacijų ir prekės ženklo -- o ThunderPhone viskuo pasirūpina viduje.

## Kada naudoti kabliuką be sąsajos

Iš anksto sukurtas `ThunderPhoneWidget` komponentas apima daugumą naudojimo atvejų, tačiau rinkitės kabliuką be sąsajos, kai reikia:

- Visiškai pritaikytos skambučio naudotojo sąsajos, atitinkančios jūsų programos dizaino sistemą
- Į garsą reaguojančių vizualizacijų (bangų formų, sferų, pulsuojančių indikatorių), valdomų pagal garso lygius realiuoju laiku
- Pritaikytų skambučio eigų, pvz., formų prieš skambutį, apklausų po skambučio ar pokalbio žinutėmis greta balso
- Integravimo į esamą komponentų biblioteką (Material UI, Chakra, Radix ir kt.)

---

## Diegimas

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

<Note>
  Kabliukui be sąsajos **nereikia** importuoti `@thunderphone/widget/style.css`, nes pateikiate savo naudotojo sąsają. Tačiau vis tiek turite įdiegti tą patį `@thunderphone/widget` paketą.
</Note>

---

## Pagrindinis naudojimas

```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>
  **Turite atvaizduoti `phone.audio` kur nors savo komponentų medyje.** Tai nematomas React elementas, valdantis pagrindinį garso ryšį. Jei jo nepridėsite, garsas nebus atkuriamas ir sesija neveiks.
</Warning>

---

## Parinktys

Perduokite šias parinktis į `useThunderPhone` naudodami `UseThunderPhoneOptions`:

| Parinktis | Tipas | Būtina | Numatytoji reikšmė | Aprašymas |
|--------|------|----------|---------|-------------|
| `publishableKey` | `string` | Taip | -- | Viešasis API raktas (`pk_live_...`). Agentas automatiškai nustatomas pagal rakto valdiklio konfigūraciją. |
| `apiBase` | `string` | Ne | `'https://api.thunderphone.com/v1'` | API bazinio URL pakeitimas. |
| `language` | `string` | Ne | -- | Vienos sesijos kalbos pakeitimas -- kalbos kodas arba lokalė, pvz., `en`, `es` arba `fr-FR`. Jei nenustatyta, taikoma sukonfigūruota agento kalba. |
| `voice` | `string` | Ne | -- | Vienos sesijos balso pakeitimas -- balso pavadinimas, pvz., `maria`. Jei nenustatyta, taikomas sukonfigūruotas agento balsas. |
| `context` | `string` | Ne | -- | Vienos sesijos faktinis puslapio arba svetainės kontekstas, perduodamas agentui. Serveryje sutrumpinamas iki 12 000 simbolių. |
| `onConnect` | `() => void` | Ne | -- | Iškviečiama prisijungus balso sesijai. |
| `onDisconnect` | `() => void` | Ne | -- | Iškviečiama pasibaigus sesijai. |
| `onError` | `(error) => void` | Ne | -- | Iškviečiama įvykus klaidoms. Klaida turi laukus `error` (kodas) ir `message`. |
| `ringtone` | `boolean \| string` | Ne | `false` | Leisti skambėjimo toną jungiantis. `true` numatytajam skambėjimo tonui arba URL eilutė pritaikytam garsui. |

<Note>
  Kabliukas yra be sąsajos: jis **nepriima** `ThunderPhoneWidget` išvaizdos rekvizitų (`theme`, `primaryColor`, `title`, `position`, `className`). Juos perduodant gaunama TypeScript klaida -- visą pateiktį kuriate jūs.
</Note>

---

## Grąžinama reikšmė

Hukas grąžina `UseThunderPhoneReturn` objektą:

| Savybė | Tipas | Aprašymas |
|----------|------|-------------|
| `state` | `'idle' \| 'connecting' \| 'connected' \| 'disconnected' \| 'error'` | Dabartinė ryšio būsena. |
| `connect` | `() => void` | Pradėkite balso seansą. |
| `disconnect` | `() => void` | Užbaikite dabartinį seansą. |
| `toggleMute` | `() => void` | Įjunkite arba išjunkite mikrofono nutildymą. |
| `isMuted` | `boolean` | Ar mikrofonas šiuo metu nutildytas. |
| `error` | `string \| undefined` | Klaidos pranešimas, kai būsena yra `'error'`. |
| `agentName` | `string \| undefined` | Prijungto agento rodomas pavadinimas. |
| `audioLevel` | `number` | **Nebenaudojama -- visada `0`.** Statinė vietaženklė reikšmė, palikta siekiant atgalinio suderinamumo; ji niekada neatnaujinama. Vietoje to nuskaitykite `audioLevelRef.current`. |
| `audioLevelRef` | `React.RefObject<number>` | Keičiamas ref, kuriame pateikiamas garso lygis realiuoju laiku (0--1) -- didesnis iš agento balso ir lankytojo mikrofono lygių -- atnaujinamas kiekviename animacijos kadre, ne React atvaizdavimo cikle. Sklandžioms, netrūkčiojančioms animacijoms nuskaitykite `audioLevelRef.current` `requestAnimationFrame` cikluose arba imkite reikšmės pavyzdžius intervalais, kai jos reikia React būsenoje. |
| `audio` | `ReactNode` | Nematomas elementas, valdantis garso ryšį -- **privalo būti atvaizduotas**. |

---

## Į garsą reaguojanti sąsaja

Nuoroda `audioLevelRef` pateikia kadrų dažnio garso lygius nesukeldama React pakartotinių atvaizdavimų, todėl puikiai tinka sklandžioms bangos formos vizualizacijoms, pulsuojantiems rutuliams ar bet kuriai su pokalbiu susietai animacijai valdyti. Lygis atspindi, kuris garsas yra stipresnis: agento balsas ar lankytojo mikrofonas.

### Bangos formos pavyzdys

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

### Pulsuojančio rutulio pavyzdys

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

### Kalbėjimo indikatoriaus pavyzdys

React atvaizduojamai sąsajai, kuri keičiasi pagal garsumą, pavyzdžiui, slenksčiu pagrįstai „kalbėjimo“ žymai, nuskaitykite `audioLevelRef.current` nustatytu intervalu ir išsaugokite rezultatą būsenoje:

```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>
  Visada nuskaitykite lygius iš `audioLevelRef.current`. Grąžinamame objekte esantis skaičius `audioLevel` yra **nebenaudojamas ir visada lygus `0`** -- bet kokia juo pagrįsta logika nepastebimai nuskaitys nulį.
</Warning>

---

## Būsenų automatas

Savybė `state` veikia pagal šį gyvavimo ciklą:

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

| Būsena | Aprašymas |
|-------|-------------|
| `idle` | Nėra aktyvios sesijos. Parengta kviesti `connect()`. |
| `connecting` | Kuriama sesija. Šios būsenos metu išjunkite skambinimo mygtuką. |
| `connected` | Balso sesija aktyvi. Naudotojas kalbasi su agentu. |
| `disconnected` | Sesija baigėsi sėkmingai. Po 1,5 sekundės automatiškai grįžta į `idle`. |
| `error` | Įvyko klaida. Pranešimą tikrinkite `phone.error`. Būsena savaime **neišsivalo** -- pakartotinai iškvietus `connect()` pradedamas naujas bandymas ir klaida nustatoma iš naujo. |

---

## Pavyzdžiai

### Su nutildymo valdymu

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

### Su skambėjimo signalu

Leiskite skambėjimo garsą jungimosi metu, kad imituotumėte telefono skambutį:

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

Skambėjimo signalas kartojamas būsenos `connecting` metu ir nutyla, kai agentas prisijungia. Perduokite `true`, jei norite naudoti integruotą numatytąjį skambėjimo signalą, arba URL eilutę, jei norite naudoti savo garso failą.

### Su įvykių atgaliniais iškvietimais

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

### Visiškai pritaikyta vartotojo sąsaja

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

---

## Patarimai

<AccordionGroup>
  <Accordion title="Visada pateikite phone.audio">
    Elementas `phone.audio` yra nematomas, tačiau būtinas. Įdėkite jį bet kur JSX kode -- jis nepateikia matomo DOM, bet viduje valdo WebRTC garso ryšį.
  </Accordion>

  <Accordion title="Jungiantis išjunkite mygtuką">
    Būsena `connecting` gali trukti 1–3 sekundes. Šios būsenos metu išjunkite skambinimo mygtuką, kad išvengtumėte pasikartojančių prisijungimo bandymų.
  </Accordion>

  <Accordion title="Tinkamai apdorokite klaidos būseną">
    Kai būsena yra `error`, parodykite naudotojui `phone.error` ir palikite skambinimo mygtuką įjungtą. Kabliukas savarankiškai neišeina iš `error` būsenos -- dar kartą iškvietus `connect()`, pradedamas naujas bandymas ir išvaloma ankstesnė klaida.
  </Accordion>

  <Accordion title="Šaliniams veiksmams naudokite atgalinius iškvietimus">
    Atgaliniai iškvietimai `onConnect`, `onDisconnect` ir `onError` idealiai tinka analitikai, žurnalų įrašams arba kitai programos logikai suaktyvinti, neapklausiant būsenos.
  </Accordion>

  <Accordion title="Garso lygius nuskaitykite iš audioLevelRef">
    `audioLevelRef` yra vienintelis tiesioginis garso lygio šaltinis. Kad animacijos, pvz., bangų formos, būtų sklandžios, nuskaitykite `audioLevelRef.current` iš `requestAnimationFrame` (ref nuskaitymas nesukelia pakartotinio atvaizdavimo), arba imkite jo reikšmę intervalais ir saugokite rezultatą būsenoje, skirtoje React atvaizduojamai sąsajai. Skaičius `audioLevel` yra pasenęs ir visada lygus `0` -- nekurdami logikos juo nesiremkite.
  </Accordion>
</AccordionGroup>
