React 组件
@openhex-ai/agent-sdk/react 让你用一个组件把 Agent 的聊天界面放进自己的网站:流式回复、附件、打断重试、历史恢复、对话内卡片都已经做好,样式自带、无需引入 CSS。 它和核心 SDK 走同一套协议,只是多了一层界面。
三个层级,按需选择
| 入口 | 是什么 | 适合 |
|---|---|---|
<ChatWidget> | 右下角的浮动气泡 + 面板,桌面端是角落面板,手机端全屏 | 网站客服、帮助中心,几行接入 |
<ChatBox> | 内嵌的聊天面板,填满父容器 | 放进你自己的页面布局 |
useOpenhexChat() | 无界面的状态 Hook | 完全自定义 UI |
安装
npm install @openhex-ai/agent-sdk react react-dom
react / react-dom(18 及以上)是 peer 依赖,用的是你项目里的那份。
快速接入
'use client';
import { ChatWidget } from '@openhex-ai/agent-sdk/react';
export default function Support() {
return (
<ChatWidget
agentId="你的-agent-id"
getToken={getToken}
persist
title="在线客服"
greeting="你好 👋 有什么可以帮你的?"
accentColor="#0f766e"
/>
);
}
getToken 从你自己的后端换取会话令牌。完整的前后端代码见示例:网站客服组件。
鉴权:浏览器里只放会话令牌
⚠️ 不要把个人 API Key(
mysta_…)或工作区 API Key(sk_…)放进浏览器。 它们等同于账号权限,打包进前端就等于公开。
组件接受三种连接方式,任选一种:
| 属性 | 说明 |
|---|---|
getToken | 推荐。 返回会话令牌的函数,组件在每次请求前调用,令牌快过期时由你换新 |
token | 静态令牌,适合每次页面加载都重新签发的场景 |
client | 一个现成的 OpenhexClient(例如指向你自己的代理),设置后其他连接属性都被忽略 |
令牌由你的后端用工作区 API Key 签发,见会话令牌。
常用属性
<ChatBox> 和 <ChatWidget> 共享下面这些属性,useOpenhexChat 接受其中的连接与对话部分。
对话
| 属性 | 默认值 | 说明 |
|---|---|---|
agentId | — | 新对话路由到的 Agent,获取方式见获取 Agent ID |
conversationId | — | 接续一段已有的对话(会加载历史) |
persist | false | 用 localStorage 记住对话,刷新页面后续上同一段对话。true 按 agentId 区分;传字符串作为自定义命名空间(例如按用户区分) |
loadHistory | true | 接续对话时是否加载历史 |
idleTimeoutMs | 180000 | 连续多久没有事件判定超时 |
senderName / senderAvatar | — | 在对话记录里标注用户的名字 / 头像 |
mcpTokens | — | 会话级 MCP 凭据,见接入外部工具 |
onTurnComplete | — | 一轮结束时回调,参数是 Agent 的回复消息 |
onError | — | 出错时回调 |
外观
| 属性 | 默认值 | 说明 |
|---|---|---|
title / subtitle / avatarUrl | — | 头部信息 |
greeting | — | 还没开始聊时显示的欢迎语(只显示,不发送) |
placeholder | — | 输入框占位文字 |
theme | light | light / dark / auto(跟随系统) |
accentColor | — | 主色(按钮、用户气泡、链接) |
tokens | — | 覆盖任意设计变量,见下文主题 |
height / width | 填满父容器 | 数字按 px |
className | — | 根元素额外的 class |
header | 内置头部 | 替换头部;false 隐藏;传函数可拿到「新对话」动作 |
emptyState | — | 没有消息时显示的节点(优先于 greeting) |
footer | 「Powered by Openhex」 | 输入框下方的小字,false 隐藏 |
injectStyles | true | 设为 false 不注入内置样式,完全自己写 |
功能开关
| 属性 | 默认值 | 说明 |
|---|---|---|
allowAttachments | true | 是否允许附件(选择、粘贴、拖拽) |
acceptFileTypes | 任意 | 文件选择框的 accept,如 "image/*" |
showToolCalls | false | 在回复下方显示 Agent 调用过的工具 |
showAudioControls | true | 在回复上显示朗读按钮,见语音回复 |
autoPlayAudio | false | 每条回复完成后自动朗读 |
showSubAgent / subAgentRenderer | 关闭 | 标注回复出自哪个 Agent,见子 Agent 与委派 |
paymentLabels / infoCollectLabels | 中文 | 覆盖卡片文案,见支付宝收款、信息收集卡片 |
disabled | false | 只读,禁止输入 |
仅 <ChatWidget>
| 属性 | 默认值 | 说明 |
|---|---|---|
position | bottom-right | bottom-right / bottom-left |
launcherIcon / launcherLabel | 内置图标 / Open chat | 气泡按钮的图标与无障碍标签 |
newConversationLabel | New chat | 头部「新对话」按钮的文字;false 去掉按钮 |
defaultOpen | false | 初始是否展开 |
open / onOpenChange | — | 受控模式 |
新对话
<ChatWidget> 头部默认带一个「新对话」按钮:清空界面、忘掉记住的对话,下一条消息在服务端开一段全新的对话。
匿名访客场景请保留这个按钮。访客身份绑在浏览器 cookie 上,没有它,下一个用这台电脑的人会接着看到上一个人的对话。只有在用户已登录、对话属于账号本身时,才考虑用
newConversationLabel={false}去掉。
自定义头部时,通过函数形式拿到这个动作:
<ChatBox
agentId="…"
getToken={getToken}
header={({ newConversation }) => (
<div className="my-header">
<span>在线客服</span>
<button onClick={newConversation}>新对话</button>
</div>
)}
/>
主题
每一处颜色、圆角、字体都来自一个 --ohx-* 设计变量。accentColor 只是其中一个的快捷方式,tokens 开放全部:
<ChatBox tokens={{ bg: '#fff', fg: '#1a1a1a', border: 'rgba(0,0,0,0.08)', radius: '12px' }} />
可用的变量:accent、accent-contrast、bg、surface、fg、muted、border、user-bg、user-fg、assistant-bg、assistant-fg、radius、font。
想和 OpenHex 网页端长得一样,套用内置预设,再按需改一两项:
import { ChatBox, PLATFORM_TOKENS } from '@openhex-ai/agent-sdk/react';
<ChatBox tokens={{ ...PLATFORM_TOKENS, accent: '#0f766e' }} />;
变量以内联样式设在组件根元素上,不需要 !important 就能覆盖内置样式。
无头 Hook:自己画界面
import { useOpenhexChat } from '@openhex-ai/agent-sdk/react';
function MyChat({ getToken }: { getToken: () => Promise<string> }) {
const { messages, send, isResponding, interrupt, retry, error } = useOpenhexChat({
agentId: '你的-agent-id',
getToken,
persist: true,
});
return (
<div>
{messages.map(m => (
<p key={m.id} className={m.role}>
{m.text}
{m.streaming && '▍'}
</p>
))}
{error && <button onClick={retry}>重试</button>}
{isResponding ? (
<button onClick={interrupt}>停止</button>
) : (
<button onClick={() => send('你好')}>发送</button>
)}
</div>
);
}
返回值:
| 字段 | 说明 |
|---|---|
messages | 按时间排列的消息列表,见下表 |
status | idle / connecting / streaming / error |
isResponding | Agent 是否正在回复 |
error | 最近一次错误,下次发送成功后清空 |
conversationId | 当前对话 id(发出第一条消息后才有) |
send(text, { files?, parts? }) | 发送消息,可带附件 |
interrupt() | 打断当前这一轮 |
retry() | 出错后重发上一条消息 |
clear() | 清空消息并忘掉对话 id |
downloadAttachment(att) | 取回 Agent 产出的文件,返回 Blob |
playAudio(message) / stopAudio() / audioPlayback | 朗读回复及其状态 |
submitInfoCollect(values) / skipInfoCollect() | 回应信息收集卡片 |
submitConnectorSetup(setup, values) | 回应连接器配置卡片 |
每条 ChatMessage:
| 字段 | 说明 |
|---|---|
id / role / text / createdAt | 基本信息;role 为 user / assistant / system |
streaming / pending / error | 正在输出 / 已发送但还没有文字 / 这一轮失败 |
attachments | 图片与文件(含 Agent 产出的文件) |
toolCalls / delegations / agent | 工具调用、委派、发言的 Agent |
paymentGate / infoCollect / connectorSetup | 对话内卡片的数据 |
页面上没有 <ChatBox>、却要用内置卡片组件(InfoCollectCard、PaymentGateCard)时,调用一次 useInjectStyles(true) 注入样式表。
服务端渲染
组件依赖浏览器 API,在 Next.js App Router 里放进带 'use client' 的组件文件中使用。