> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/react/custom-ui-and-advanced-usage.md).

# 在 React 中构建自定义 UI

用你自己的 React 消息和控制组件替换预构建小组件，同时由 Convai Web SDK 继续处理音频和状态。

在 React 中基于……构建你自己的对话界面 `useConvaiClient` 而不是渲染 `ConvaiWidget`. 当小部件的布局不适合你的应用时，请使用此页面。最终，你的组件将显示对话、发送文本，并控制麦克风、摄像头和屏幕共享。

### 先决条件

* 已安装 Convai Web SDK 的 React 应用。参见 [开始使用 React](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/react.md).
* 一个 `useConvaiClient` 用于对话的调用。只创建一次客户端，放在组件树的靠上位置，并向下传递；每次调用该 hook 都会创建一个具有自己连接的独立客户端。

### 播放角色的音频

没有该小部件时，默认的 LiveKit 传输不会播放角色的声音。渲染 `AudioRenderer` 在……内部 `AudioContext.Provider` 并使用客户端的房间。

{% code title="src/App.tsx" %}

```tsx
import { useConvaiClient } from '@convai/web-sdk/react';
import { AudioRenderer, AudioContext } from '@convai/web-widget/react';
import { Conversation } from './Conversation';

export default function App() {
  const convaiClient = useConvaiClient({
    apiKey: 'YOUR_API_KEY',
    characterId: 'YOUR_CHARACTER_ID',
  });

  return (
    <AudioContext.Provider value={convaiClient.room}>
      <AudioRenderer />
      <Conversation convaiClient={convaiClient} />
    </AudioContext.Provider>
  );
}
```

{% endcode %}

`AudioRenderer` 并且 `AudioContext` 来自 `@convai/web-widget/react` 是 LiveKit 的 `RoomAudioRenderer` 并且 `RoomContext` 在 SDK 名称下。 `AudioContext` 这里不是浏览器的 Web Audio `AudioContext`；如果同一个文件也使用 Web Audio，请用另一个名称导入它。

浏览器会阻止声音，直到访问者在页面上点击、轻触或按下按键。如果你的应用在页面加载时就连接，那么在此之前角色可能没有声音，而这 `AudioRenderer` 不会自行开始播放。请在首次交互时恢复它：

```tsx
useEffect(() => {
  const start = () => void client.room.startAudio();
  addEventListener('pointerdown', start, { once: true });
  addEventListener('keydown', start, { once: true });
  return () => {
    removeEventListener('pointerdown', start);
    removeEventListener('keydown', start);
  };
}, [client]);
```

通过点击进行连接，例如 **开始** 按钮，可以完全避免这个问题。

使用 `transport: "websocket"`时，Pipecat 传输会通过 Web Audio 自行播放声音，并且 `AudioRenderer` 不需要它。

### 渲染消息

`chatMessages` 包含 SDK 记录的所有消息类型。对于聊天视图，请保留用户输入的文本和转写语音，以及角色的文本。

{% code title="src/Conversation.tsx" %}

```tsx
import { useState } from 'react';
import type { useConvaiClient } from '@convai/web-sdk/react';

type Client = ReturnType<typeof useConvaiClient>;

const SHOWN = new Set(['user-llm-text', 'user-transcription', 'bot-llm-text', 'bot-output']);

export function Conversation({ convaiClient }: { convaiClient: Client }) {
  const [text, setText] = useState('');
  const canSend = convaiClient.state.isConnected && convaiClient.isBotReady;

  const send = () => {
    if (!text.trim() || !canSend) return;
    convaiClient.sendUserTextMessage(text);
    setText('');
  };

  return (
    <div>
      {!convaiClient.state.isConnected && (
        <button onClick={() => convaiClient.connect()}>连接</button>
      )}

      <ul>
        {convaiClient.chatMessages
          .filter((m) => SHOWN.has(m.type) && m.content.trim())
          .map((m) => (
            <li key={m.id} className={m.type.startsWith('user') ? 'user' : 'character'}>
              {m.content}
            </li>
          ))}
      </ul>

      <p>{convaiClient.userTranscription}</p>

      <input value={text} onChange={(e) => setText(e.target.value)} disabled={!canSend} />
      <button onClick={send} disabled={!canSend}>发送</button>

      <Controls convaiClient={convaiClient} />
    </div>
  );
}
```

{% endcode %}

| 类型                   | 包含                                         |
| -------------------- | ------------------------------------------ |
| `user-llm-text`      | 你发送的文本，使用 `sendUserTextMessage`，发送时在本地添加。  |
| `user-transcription` | 你语音的最终转写。实时的部分转写位于 `userTranscription`.    |
| `bot-llm-text`       | 角色的回复，流式传输。 `isStreaming` 是 `true` 直到回复完成。 |
| `bot-output`         | 角色的口语文本，在没有 `bot-llm-text` 消息承载回复时记录。      |

完整的消息类型列表位于 [配置与类型参考](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/vanilla-typescript/best-practices-and-type-definitions.md).

### 添加媒体控制

这些控制对象不是 React 状态，因此它们的字段，例如 `audioControls.isAudioMuted`，在它们变化时不会重新渲染你的组件。请读取该 hook 的响应式 `isAudioMuted`, `isVideoEnabled`，以及 `isScreenShareActive` 数据，并调用控制方法来更改它们。

{% code title="src/Controls.tsx" %}

```tsx
import type { useConvaiClient } from '@convai/web-sdk/react';

type Client = ReturnType<typeof useConvaiClient>;

export function Controls({ convaiClient }: { convaiClient: Client }) {
  const { audioControls, videoControls, screenShareControls } = convaiClient;
  const hasVideo = convaiClient.connectionType === 'video';

  return (
    <>
      <button onClick={() => audioControls.toggleAudio()}>
        {convaiClient.isAudioMuted ? '取消静音' : '静音'}
      </button>

      {hasVideo && (
        <button onClick={() => videoControls.toggleVideo()}>
          {convaiClient.isVideoEnabled ? '关闭摄像头' : '开启摄像头'}
        </button>
      )}

      {hasVideo && (
        <button onClick={() => screenShareControls.toggleScreenShare()}>
          {convaiClient.isScreenShareActive ? '停止共享' : '共享屏幕'}
        </button>
      )}
    </>
  );
}
```

{% endcode %}

控制方法会返回 promise，并且可能被拒绝，例如当浏览器拒绝麦克风或摄像头访问时。摄像头和屏幕共享需要使用 `enableVideo: true`.

### 验证自定义 UI

选择 **连接**，等待输入框可用，然后发送一条消息。

{% hint style="success" %}
你的消息会出现在列表中，角色的回复会在其下方流式显示，并且你会听到角色的声音。
{% endhint %}

### 后续步骤

{% content-ref url="/pages/68959b8b127b7dad992c41de6f7e87cd945ce3e3" %}
[useConvaiClient Hook 参考](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/react/useconvaiclient-hook.md)
{% endcontent-ref %}

{% content-ref url="/pages/8c6e3631e7e7da5b3c9acf5a0019d7737dc79e19" %}
[Web SDK 事件参考](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/event-reference.md)
{% endcontent-ref %}

{% content-ref url="/pages/1ef8009eea712d4c41fd7a2a07e473a277395698" %}
[处理错误和断开连接](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/error-handling.md)
{% endcontent-ref %}

{% content-ref url="/pages/40347340c726b64e9b42d20d133807f8d73d6014" %}
[在 React 中添加音频可视化器](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/react/audio-visualizer-optional.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/react/custom-ui-and-advanced-usage.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
