#OpenUI 渲染
后端返回一段 OpenUI 描述(声明式的界面 DSL),chatkit 把它渲染成指标卡、图表、表格、表单等组件。适合报告、看板这类需要结构化展示、又不值得为每种版式写业务卡片的回答。
OpenUI 是按需引入的功能:它不在默认入口里,不引入就不会进包。
openui.tsx
import '@volcengine/chatkit/styles.css';
import '@volcengine/chatkit/openui.css';
import './openui-demo.css';
import { Chatkit, type ChatkitDataPartRenderer, useChatRuntime } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import { OpenUiArtifact } from '@volcengine/chatkit/openui';
const OPENUI_PART_KIND = 'demo.openui';
const OPENUI_SOURCE = `root = Stack([header, status, metrics, charts, tableTitle, table, insight])
header = CardHeader("Q3 服务运行报告", "2026 年第三季度 · API 网关与推理服务 · 数据截至 9 月 30 日")
status = Callout("success", "整体运行稳定", "核心服务可用性 99.94%,错误率低于 0.1%。本报告展示标题、状态、KPI、图表、表格和说明组件。")
metrics = Stack([requestCard, successCard, latencyCard, costCard], "row", "m", "stretch", "start", true)
requestCard = Card([TextContent("总请求量", "small"), TextContent("148.2 万", "large-heavy"), TextContent("较上季度 +12.8%", "small")])
successCard = Card([TextContent("请求成功率", "small"), TextContent("99.94%", "large-heavy"), TextContent("较上季度 +0.3 个百分点", "small")])
latencyCard = Card([TextContent("P95 延迟", "small"), TextContent("428 ms", "large-heavy"), TextContent("目标 < 500 ms", "small")])
costCard = Card([TextContent("推理成本", "small"), TextContent("¥ 82,460", "large-heavy"), TextContent("单位请求成本 -6.2%", "small")])
charts = Stack([trafficCard, latencyCardSection, channelCard], "column", "m")
trafficCard = Card([CardHeader("月度请求趋势", "请求量持续增长,峰值出现在 9 月"), trafficChart])
trafficChart = LineChart(months, [requestSeries, previousSeries])
requestSeries = Series("本季度请求量(万)", [41.2, 44.8, 48.7, 51.3, 55.6, 60.1, 66.4])
previousSeries = Series("上季度对比(万)", [38.1, 40.5, 43.2, 45.7, 48.4, 52.8, 58.2])
months = ["3 月", "4 月", "5 月", "6 月", "7 月", "8 月", "9 月"]
latencyCardSection = Card([CardHeader("核心服务 P95 延迟", "当前值与上季度对比,单位:毫秒"), latencyChart])
latencyChart = BarChart(services, [currentLatency, previousLatency], "grouped")
services = ["对话 API", "检索服务", "模型网关", "文件解析"]
currentLatency = Series("本季度", [428, 362, 486, 612])
previousLatency = Series("上季度", [471, 405, 522, 648])
channelCard = Card([CardHeader("请求来源", "本季度总请求占比"), channelChart])
channelChart = PieChart(channels, channelValues, "donut")
channels = ["Web", "API", "移动端", "内部任务"]
channelValues = [46, 28, 18, 8]
tableTitle = TextContent("服务质量明细", "large-heavy")
table = Table([Col("服务", serviceNames), Col("成功率", successRates), Col("P95 延迟", serviceLatency), Col("状态", serviceStatus)])
serviceNames = ["对话 API", "检索服务", "模型网关", "文件解析", "任务调度"]
successRates = ["99.97%", "99.91%", "99.95%", "99.72%", "99.99%"]
serviceLatency = ["428 ms", "362 ms", "486 ms", "612 ms", "94 ms"]
serviceStatus = ["正常", "正常", "正常", "关注", "正常"]
insight = TextCallout("info", "分析结论", "文件解析服务的 P95 延迟高于 500 ms 目标,是本季度唯一需要跟进的指标。计划通过并行读取和缓存优化,在下季度将其降低至 500 ms 以内。")`;
const adapter = createMockAdapter({
transformOutput: (step, context) => {
if (!context.messageId || step.kind !== 'data') return undefined;
if (step.dataKind !== OPENUI_PART_KIND) return undefined;
return [
{
type: 'set-part',
messageId: context.messageId,
part: { id: 'openui', type: 'data', kind: OPENUI_PART_KIND, data: { source: OPENUI_SOURCE } },
},
];
},
getScenarioId: () => 'openui',
});
interface OpenUiPartData {
source: string;
}
const renderOpenUi: ChatkitDataPartRenderer = ({ data, isRunning }) => {
const openUi = data as OpenUiPartData;
return <OpenUiArtifact source={openUi.source} isStreaming={isRunning} title="OpenUI 示例" />;
};
export default function OpenUi() {
const runtime = useChatRuntime({ sessionId: 'openui', adapter });
if (!runtime) return null;
return (
<div className="demo-openui">
<Chatkit
runtime={runtime}
root={{ style: { maxWidth: 920, margin: '0 auto' } }}
settings={{
welcome: '发送问题后,Mock 回答会返回结构化 OpenUI 内容。',
recommendations: ['展示 OpenUI 渲染示例'],
}}
renderers={{ parts: { [OPENUI_PART_KIND]: renderOpenUi } }}
/>
</div>
);
}Mock 回答返回一段 OpenUI,渲染成服务运行报告新窗口打开
#引入
import '@volcengine/chatkit/styles.css';
import '@volcengine/chatkit/openui.css';
import { OpenUiArtifact } from '@volcengine/chatkit/openui';openui.css 只包含 OpenUI 组件的样式;主题变量与基础样式仍来自 styles.css。
#用法一:作为业务卡片
后端把 OpenUI 源码放在一块数据里返回,用 renderers.parts 登记显示组件。数据怎么从后端到达这一步,见业务卡片。
import type { ChatkitDataPartRenderer } from '@volcengine/chatkit';
import { OpenUiArtifact } from '@volcengine/chatkit/openui';
const OpenUiCard: ChatkitDataPartRenderer = ({ data, isRunning }) => (
<OpenUiArtifact source={(data as { source: string }).source} isStreaming={isRunning} title="运行报告" />
);
<Chatkit sessions={sessions} renderers={{ parts: { 'acme.openui': OpenUiCard } }} />;#用法二:写在回答正文里
后端直接在 Markdown 正文里输出一个标签,用 renderers.markdown 把标签名登记为 OpenUiArtifact。标签名可以自定,下面用 openui-artifact:
<Chatkit sessions={sessions} renderers={{ markdown: { 'openui-artifact': OpenUiArtifact } }} />本季度运行情况如下:
<openui-artifact title="运行报告">
root = Stack([header, metrics])
header = CardHeader("Q3 服务运行报告", "数据截至 9 月 30 日")
metrics = Stack([TextContent("总请求量", "small"), TextContent("148.2 万", "large-heavy")])
</openui-artifact>标签前后各空一行。标签里的文本就是 OpenUI 源码;title 属性显示为标题。
#组件参数
| 参数 | 说明 |
|---|---|
source | OpenUI 源码。写在 Markdown 标签里时不用传,取标签内的文本 |
title | 显示在内容上方的标题 |
isStreaming | 是否还在生成。在回答里使用时默认跟随所在消息的状态,在回答之外使用时传入 |
#显示规则
- 流式输出:回答还在生成时,内容边到达边渲染;源码暂时不完整不会显示错误。
- 只渲染白名单里的组件:支持布局、文本、图表、表格、标签页、表单控件、图片等组件,完整列表从
OPENUI_COMPONENT_ALLOWLIST读取。源码里出现名单外的组件时,整块不渲染。 - 无法渲染时显示源码:组件不在白名单、源码在生成结束后仍无法解析时,显示「OpenUI 内容无法解析」和原始源码;源码超过 200,000 个字符时显示「内容超过预览限制」。
- 主题:跟随最近的
data-chatkit-theme(浅色或深色)。
import { OPENUI_COMPONENT_ALLOWLIST } from '@volcengine/chatkit/openui';
// 写进给模型的提示词,让它只用支持的组件
const prompt = `只能使用这些组件:${OPENUI_COMPONENT_ALLOWLIST.join('、')}`;#在聊天区之外使用
OpenUiArtifact 也可以放在聊天区之外,例如右侧面板或独立页面。它自带 chatkit 的样式作用域;要和聊天区共用深色或自定义主题,把它放在同一个 ChatkitProvider 下面,或放在带 data-chatkit-theme 的容器里。