name: building-chat-widgets description: | 构建具有按钮、表单和双向操作的交互式AI聊天小部件。 适用于创建具有可点击小部件、实体标记(@提及)、 编辑器工具或服务器处理的小部件操作的智能代理UI。涵盖完整的小部件生命周期。 不适用于构建没有交互元素的纯文本聊天。
构建聊天小部件
创建具有操作和实体标记功能的交互式AI聊天小部件。
快速开始
const chatkit = useChatKit({
api: { url: API_URL, domainKey: DOMAIN_KEY },
widgets: {
onAction: async (action, widgetItem) => {
if (action.type === "view_details") {
navigate(`/details/${action.payload.id}`);
}
},
},
});
操作处理器类型
| 处理器 | 定义位置 | 处理方 | 使用场景 |
|---|---|---|---|
"client" |
小部件模板 | 前端 onAction |
导航、本地状态 |
"server" |
小部件模板 | 后端 action() |
数据变更、小部件替换 |
小部件生命周期
1. 代理工具生成小部件 → 产出 WidgetItem
2. 小部件在聊天中渲染并带有操作按钮
3. 用户点击操作 → 操作被分发
4. 处理器处理操作:
- client: 前端中的 onAction 回调
- server: ChatKitServer 中的 action() 方法
5. 可选:小部件被更新状态替换
核心模式
1. 小部件模板
定义具有动态数据的可复用小部件布局:
{
"type": "ListView",
"children": [
{
"type": "ListViewItem",
"key": "item-1",
"onClickAction": {
"type": "item.select",
"handler": "client",
"payload": { "itemId": "item-1" }
},
"children": [
{
"type": "Row",
"gap": 3,
"children": [
{ "type": "Icon", "name": "check", "color": "success" },
{ "type": "Text", "value": "Item title", "weight": "semibold" }
]
}
]
}
]
}
2. 客户端处理的操作
更新本地状态、导航或发送后续消息的操作:
小部件定义:
{
"type": "Button",
"label": "查看文章",
"onClickAction": {
"type": "open_article",
"handler": "client",
"payload": { "id": "article-123" }
}
}
前端处理器:
const chatkit = useChatKit({
api: { url: API_URL, domainKey: DOMAIN_KEY },
widgets: {
onAction: async (action, widgetItem) => {
switch (action.type) {
case "open_article":
navigate(`/article/${action.payload?.id}`);
break;
case "more_suggestions":
await chatkit.sendUserMessage({ text: "请提供更多建议" });
break;
case "select_option":
setSelectedOption(action.payload?.optionId);
break;
}
},
},
});
3. 服务器端处理的操作
变更数据、更新小部件或需要后端处理的操作:
小部件定义:
{
"type": "ListViewItem",
"onClickAction": {
"type": "line.select",
"handler": "server",
"payload": { "id": "blue-line" }
}
}
后端处理器:
from chatkit.types import (
Action, WidgetItem, ThreadItemReplacedEvent,
ThreadItemDoneEvent, AssistantMessageItem, ClientEffectEvent,
)
class MyServer(ChatKitServer[dict]):
async def action(
self,
thread: ThreadMetadata,
action: Action[str, Any],
sender: WidgetItem | None,
context: RequestContext, # 注意:已经是 RequestContext,不是 dict
) -> AsyncIterator[ThreadStreamEvent]:
if action.type == "line.select":
line_id = action.payload["id"] # 使用 .payload,而不是 .arguments
# 1. 更新小部件选择
updated_widget = build_selector_widget(selected=line_id)
yield ThreadItemReplacedEvent(
item=sender.model_copy(update={"widget": updated_widget})
)
# 2. 流式输出助手消息
yield ThreadItemDoneEvent(
item=AssistantMessageItem(
id=self.store.generate_item_id("msg", thread, context),
thread_id=thread.id,
created_at=datetime.now(),
content=[{"text": f"已选择 {line_id}"}],
)
)
# 3. 触发客户端效果
yield ClientEffectEvent(
name="selection_changed",
data={"lineId": line_id},
)
4. 实体标记(@提及)
允许用户在消息中@提及实体:
const chatkit = useChatKit({
api: { url: API_URL, domainKey: DOMAIN_KEY },
entities: {
onTagSearch: async (query: string): Promise<Entity[]> => {
const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
return results.map((item) => ({
id: item.id,
title: item.name,
icon: item.type === "person" ? "profile" : "document",
group: item.type === "People" ? "People" : "Articles",
interactive: true,
data: { type: item.type, article_id: item.id },
}));
},
onClick: (entity: Entity) => {
if (entity.data?.article_id) {
navigate(`/article/${entity.data.article_id}`);
}
},
},
});
5. 编辑器工具(模式选择)
让用户从编辑器中选择不同的AI模式:
const TOOL_CHOICES = [
{
id: "general",
label: "聊天",
icon: "sparkle",
placeholderOverride: "问任何问题...",
pinned: true,
},
{
id: "event_finder",
label: "查找活动",
icon: "calendar",
placeholderOverride: "您想找什么活动?",
pinned: true,
},
];
const chatkit = useChatKit({
api: { url: API_URL, domainKey: DOMAIN_KEY },
composer: {
placeholder: "您想做什么?",
tools: TOOL_CHOICES,
},
});
后端路由:
async def respond(self, thread, item, context):
tool_choice = context.metadata.get("tool_choice")
if tool_choice == "event_finder":
agent = self.event_finder_agent
else:
agent = self.general_agent
result = Runner.run_streamed(agent, input_items)
async for event in stream_agent_response(context, result):
yield event
小部件组件参考
布局组件
| 组件 | 属性 | 描述 |
|---|---|---|
ListView |
children |
可滚动列表容器 |
ListViewItem |
key, onClickAction, children |
可点击列表项 |
Row |
gap, align, justify, children |
水平弹性布局 |
Col |
gap, padding, children |
垂直弹性布局 |
Box |
size, radius, background, padding |
样式化容器 |
内容组件
| 组件 | 属性 | 描述 |
|---|---|---|
Text |
value, size, weight, color |
文本显示 |
Title |
value, size, weight |
标题文本 |
Image |
src, alt, width, height |
图像显示 |
Icon |
name, size, color |
图标集 |
交互式组件
| 组件 | 属性 | 描述 |
|---|---|---|
Button |
label, variant, onClickAction |
可点击按钮 |
关键实现细节
操作对象结构
重要:使用 action.payload,而不是 action.arguments:
# 错误 - 会导致 AttributeError
action.arguments
# 正确
action.payload
上下文参数
context 参数是 RequestContext,不是 dict:
# 错误 - 尝试包装 RequestContext
request_context = RequestContext(metadata=context)
# 正确 - 直接使用
user_id = context.user_id
UserMessageItem 必填字段
创建合成用户消息时:
from chatkit.types import UserMessageItem, UserMessageTextContent
# 包含所有必填字段
synthetic_message = UserMessageItem(
id=self.store.generate_item_id("message", thread, context),
thread_id=thread.id,
created_at=datetime.now(),
content=[UserMessageTextContent(type="input_text", text=message_text)],
inference_options={},
)
反模式
- 混合处理器 - 不要同时在客户端和服务器处理相同的操作
- 缺少 payload - 始终在操作 payload 中包含数据
- 使用 action.arguments - 使用
action.payload - 包装 RequestContext - Context 已经是 RequestContext
- 缺少 UserMessageItem 字段 - 包含 id, thread_id, created_at
- 错误的内容类型 - 用户消息使用
type="input_text"
验证
运行:python3 scripts/verify.py
预期:✓ building-chat-widgets 技能就绪
如果验证失败
- 检查:references/ 文件夹中有 widget-patterns.md
- 如果仍然失败,请停止并报告
参考资料
- references/widget-patterns.md - 完整的小部件模式
- references/server-action-handler.md - 后端操作处理