构建聊天小部件 building-chat-widgets

本技能指南详细介绍了如何为AI聊天应用构建交互式小部件,包括按钮、表单、实体标记(@提及)和双向操作。它涵盖了从小部件模板定义、客户端与服务器端操作处理、实体标记功能到编辑器工具集成的完整生命周期。适用于开发具有丰富交互元素的智能代理UI、聊天机器人界面和对话式应用,帮助开发者实现可点击组件、数据驱动的UI更新和前后端协同的复杂交互逻辑。关键词:AI聊天小部件,交互式UI,实体标记,双向操作,ChatKit,前端开发,后端集成,智能代理。

前端开发 0 次安装 0 次浏览 更新于 3/2/2026

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={},
)

反模式

  1. 混合处理器 - 不要同时在客户端和服务器处理相同的操作
  2. 缺少 payload - 始终在操作 payload 中包含数据
  3. 使用 action.arguments - 使用 action.payload
  4. 包装 RequestContext - Context 已经是 RequestContext
  5. 缺少 UserMessageItem 字段 - 包含 id, thread_id, created_at
  6. 错误的内容类型 - 用户消息使用 type="input_text"

验证

运行:python3 scripts/verify.py

预期:✓ building-chat-widgets 技能就绪

如果验证失败

  1. 检查:references/ 文件夹中有 widget-patterns.md
  2. 如果仍然失败,请停止并报告

参考资料