> ## 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 Hook

> Xây dựng giao diện giọng nói tùy chỉnh hoàn toàn với React hook useThunderPhone

Hook `useThunderPhone` cho phép bạn kiểm soát hoàn toàn giao diện người dùng, trong khi ThunderPhone quản lý phiên thoại, định tuyến âm thanh và trạng thái kết nối. Sử dụng hook này khi bạn muốn có UI tùy chỉnh hoàn toàn -- với các nút, bố cục, hiệu ứng động và thương hiệu riêng -- trong khi ThunderPhone xử lý mọi thứ phía sau.

## Khi nào nên sử dụng Hook không giao diện

Component `ThunderPhoneWidget` dựng sẵn đáp ứng hầu hết trường hợp sử dụng, nhưng hãy dùng hook không giao diện khi bạn cần:

* UI cuộc gọi tùy chỉnh hoàn toàn, phù hợp với hệ thống thiết kế của ứng dụng
* Hình ảnh trực quan phản ứng theo âm thanh (dạng sóng, quả cầu, chỉ báo nhấp nháy) được điều khiển bởi mức âm thanh theo thời gian thực
* Luồng cuộc gọi tùy chỉnh như biểu mẫu trước cuộc gọi, khảo sát sau cuộc gọi hoặc chat nội tuyến cùng với thoại
* Tích hợp vào thư viện component hiện có (Material UI, Chakra, Radix, v.v.)

***

## Cài đặt

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

<Note>
  Hook không giao diện **không** yêu cầu import `@thunderphone/widget/style.css` vì bạn tự cung cấp UI. Tuy nhiên, bạn vẫn phải cài đặt cùng package `@thunderphone/widget`.
</Note>

***

## Cách sử dụng cơ bản

```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>
  **Bạn phải render `phone.audio` ở đâu đó trong cây component của mình.** Đây là một phần tử React vô hình quản lý kết nối âm thanh nền tảng. Nếu bỏ qua, âm thanh sẽ không phát và phiên sẽ không hoạt động.
</Warning>

***

## Tùy chọn

Truyền các tùy chọn này vào `useThunderPhone` thông qua `UseThunderPhoneOptions`:

| Tùy chọn         | Kiểu                | Bắt buộc | Mặc định                            | Mô tả                                                                                                                                                        |
| ---------------- | ------------------- | -------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `publishableKey` | `string`            | Có       | --                                  | API key có thể công khai (`pk_live_...`). Tác nhân AI được xác định tự động từ cấu hình widget của key.                                                      |
| `apiBase`        | `string`            | Không    | `'https://api.thunderphone.com/v1'` | Ghi đè URL cơ sở API.                                                                                                                                        |
| `language`       | `string`            | Không    | --                                  | Ghi đè ngôn ngữ theo từng phiên -- mã ngôn ngữ hoặc locale như `en`, `es` hoặc `fr-FR`. Khi không đặt, ngôn ngữ đã cấu hình của tác nhân AI sẽ được áp dụng. |
| `voice`          | `string`            | Không    | --                                  | Ghi đè giọng nói theo từng phiên -- tên giọng nói như `maria`. Khi không đặt, giọng nói đã cấu hình của tác nhân AI sẽ được áp dụng.                         |
| `context`        | `string`            | Không    | --                                  | Ngữ cảnh thực tế của trang hoặc website theo từng phiên được truyền đến tác nhân AI. Được cắt ngắn phía máy chủ xuống còn 12.000 ký tự.                      |
| `onConnect`      | `() => void`        | Không    | --                                  | Được gọi khi phiên thoại kết nối.                                                                                                                            |
| `onDisconnect`   | `() => void`        | Không    | --                                  | Được gọi khi phiên kết thúc.                                                                                                                                 |
| `onError`        | `(error) => void`   | Không    | --                                  | Được gọi khi xảy ra lỗi. Lỗi có các trường `error` (mã) và `message`.                                                                                        |
| `ringtone`       | `boolean \| string` | Không    | `false`                             | Phát nhạc chuông trong khi kết nối. `true` dùng nhạc chuông mặc định, hoặc chuỗi URL cho âm thanh tùy chỉnh.                                                 |

<Note>
  Hook này không giao diện: nó **không** chấp nhận các prop về giao diện của `ThunderPhoneWidget` (`theme`, `primaryColor`, `title`, `position`, `className`). Truyền các prop này sẽ gây lỗi TypeScript -- phần trình bày hoàn toàn do bạn xây dựng.
</Note>

***

## Giá trị trả về

Hook trả về một đối tượng `UseThunderPhoneReturn`:

| Thuộc tính      | Loại                                                                 | Mô tả                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state`         | `'idle' \| 'connecting' \| 'connected' \| 'disconnected' \| 'error'` | Trạng thái kết nối hiện tại.                                                                                                                                                                                                                                                                                                                                                                                                |
| `connect`       | `() => void`                                                         | Bắt đầu phiên thoại.                                                                                                                                                                                                                                                                                                                                                                                                        |
| `disconnect`    | `() => void`                                                         | Kết thúc phiên hiện tại.                                                                                                                                                                                                                                                                                                                                                                                                    |
| `toggleMute`    | `() => void`                                                         | Bật/tắt chế độ tắt tiếng microphone.                                                                                                                                                                                                                                                                                                                                                                                        |
| `isMuted`       | `boolean`                                                            | Cho biết microphone hiện đang bị tắt tiếng hay không.                                                                                                                                                                                                                                                                                                                                                                       |
| `error`         | `string \| undefined`                                                | Thông báo lỗi khi trạng thái là `'error'`.                                                                                                                                                                                                                                                                                                                                                                                  |
| `agentName`     | `string \| undefined`                                                | Tên hiển thị của tác nhân AI đã kết nối.                                                                                                                                                                                                                                                                                                                                                                                    |
| `audioLevel`    | `number`                                                             | **Đã ngừng hỗ trợ -- luôn là `0`.** Đây là giá trị giữ chỗ tĩnh để tương thích ngược; giá trị này không bao giờ cập nhật. Thay vào đó, hãy đọc `audioLevelRef.current`.                                                                                                                                                                                                                                                     |
| `audioLevelRef` | `React.RefObject<number>`                                            | Một ref có thể thay đổi chứa mức âm thanh theo thời gian thực (0--1) -- mức lớn hơn giữa giọng nói của tác nhân AI và microphone của khách truy cập -- được cập nhật ở mỗi khung hình hoạt ảnh, bên ngoài chu kỳ render của React. Đọc `audioLevelRef.current` trong các vòng lặp `requestAnimationFrame` để có hoạt ảnh mượt mà, không giật, hoặc lấy mẫu theo khoảng thời gian khi bạn cần giá trị trong state của React. |
| `audio`         | `ReactNode`                                                          | Phần tử vô hình xử lý kết nối âm thanh -- **bắt buộc phải render**.                                                                                                                                                                                                                                                                                                                                                         |

***

## Giao diện phản hồi theo âm thanh

Ref `audioLevelRef` cung cấp mức âm thanh theo tốc độ khung hình mà không kích hoạt React render lại, nên rất phù hợp để điều khiển trực quan hóa dạng sóng mượt mà, các quả cầu nhấp nhô hoặc bất kỳ hoạt ảnh nào gắn với cuộc trò chuyện. Mức này phản ánh âm thanh nào lớn hơn: giọng nói của tác nhân AI hoặc microphone của khách truy cập.

### Ví dụ dạng sóng

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

### Ví dụ quả cầu nhấp nhô

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

### Ví dụ chỉ báo đang nói

Đối với giao diện được React render thay đổi theo âm lượng -- chẳng hạn như nhãn "đang nói" dựa trên ngưỡng -- hãy lấy mẫu `audioLevelRef.current` theo một khoảng thời gian và lưu kết quả vào state:

```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>
  Luôn đọc mức từ `audioLevelRef.current`. Số `audioLevel` trên đối tượng trả về đã **không còn được dùng và luôn là `0`** -- mọi logic xây dựng dựa trên nó sẽ âm thầm đọc giá trị bằng không.
</Warning>

***

## Máy trạng thái

Thuộc tính `state` tuân theo vòng đời sau:

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

| Trạng thái     | Mô tả                                                                                                                                                          |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `idle`         | Không có phiên hoạt động. Sẵn sàng gọi `connect()`.                                                                                                            |
| `connecting`   | Phiên đang được thiết lập. Vô hiệu hóa nút gọi trong trạng thái này.                                                                                           |
| `connected`    | Phiên thoại đang hoạt động. Người dùng đang nói chuyện với tác nhân AI.                                                                                        |
| `disconnected` | Phiên đã kết thúc bình thường. Tự động chuyển lại `idle` sau 1,5 giây.                                                                                         |
| `error`        | Đã xảy ra lỗi. Kiểm tra `phone.error` để xem thông báo. Trạng thái này **không** tự xóa -- việc gọi lại `connect()` sẽ bắt đầu một lần thử mới và đặt lại lỗi. |

***

## Ví dụ

### Với điều khiển tắt tiếng

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

### Với nhạc chuông

Phát âm thanh đổ chuông trong khi kết nối để mô phỏng cuộc gọi điện thoại:

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

Nhạc chuông sẽ lặp lại trong trạng thái `connecting` và nhỏ dần khi tác nhân AI kết nối. Truyền `true` để dùng nhạc chuông mặc định tích hợp sẵn hoặc truyền chuỗi URL để dùng tệp âm thanh của riêng bạn.

### Với callback sự kiện

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

### Giao diện tùy chỉnh hoàn toàn

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

***

## Mẹo

<AccordionGroup>
  <Accordion title="Luôn render phone.audio">
    Phần tử `phone.audio` không hiển thị nhưng bắt buộc. Đặt phần tử này ở bất kỳ đâu trong JSX của bạn -- phần tử không render DOM hiển thị nhưng quản lý kết nối âm thanh WebRTC nội bộ.
  </Accordion>

  <Accordion title="Vô hiệu hóa nút khi đang kết nối">
    Trạng thái `connecting` có thể kéo dài 1-3 giây. Vô hiệu hóa nút gọi trong trạng thái này để ngăn các lần thử kết nối trùng lặp.
  </Accordion>

  <Accordion title="Xử lý trạng thái lỗi một cách hợp lý">
    Khi trạng thái là `error`, hãy hiển thị `phone.error` cho người dùng và giữ nút gọi của bạn ở trạng thái bật. Hook không tự thoát khỏi trạng thái `error` -- việc gọi lại `connect()` sẽ bắt đầu một lần thử mới và xóa lỗi trước đó.
  </Accordion>

  <Accordion title="Dùng callback cho các tác dụng phụ">
    Các callback `onConnect`, `onDisconnect` và `onError` phù hợp cho phân tích, ghi log hoặc kích hoạt logic ứng dụng khác mà không cần thăm dò trạng thái.
  </Accordion>

  <Accordion title="Đọc mức âm thanh từ audioLevelRef">
    `audioLevelRef` là nguồn mức âm thanh trực tiếp duy nhất. Đọc `audioLevelRef.current` bên trong `requestAnimationFrame` để tạo hiệu ứng mượt mà như dạng sóng (đọc ref không gây render lại), hoặc lấy mẫu theo khoảng thời gian và lưu kết quả vào state cho UI do React render. Số `audioLevel` đã bị ngừng sử dụng và luôn là `0` -- không xây dựng logic dựa trên nó.
  </Accordion>
</AccordionGroup>
