building-chat-widgets
ボタンやフォーム、双方向アクションを備えたAIチャットUIを構築し、エンティティのタグ付けやウィジェット操作など、インタラクティブな要素を伴うエージェント的なUIを開発するSkill。
📜 元の英語説明(参考)
Build interactive AI chat widgets with buttons, forms, and bidirectional actions. Use when creating agentic UIs with clickable widgets, entity tagging (@mentions), composer tools, or server-handled widget actions. Covers full widget lifecycle. NOT when building simple text-only chat without interactive elements.
🇯🇵 日本人クリエイター向け解説
ボタンやフォーム、双方向アクションを備えたAIチャットUIを構築し、エンティティのタグ付けやウィジェット操作など、インタラクティブな要素を伴うエージェント的なUIを開発するSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o building-chat-widgets.zip https://jpskill.com/download/17285.zip && unzip -o building-chat-widgets.zip && rm building-chat-widgets.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17285.zip -OutFile "$d\building-chat-widgets.zip"; Expand-Archive "$d\building-chat-widgets.zip" -DestinationPath $d -Force; ri "$d\building-chat-widgets.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
building-chat-widgets.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
building-chat-widgetsフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 4
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
チャットウィジェットの構築
アクションとエンティティのタグ付けを使用して、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 を yield
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": "View Article",
"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: "More suggestions, please" });
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, # Note: Already RequestContext, not dict
) -> AsyncIterator[ThreadStreamEvent]:
if action.type == "line.select":
line_id = action.payload["id"] # Use .payload, not .arguments
# 1. Update widget with selection
updated_widget = build_selector_widget(selected=line_id)
yield ThreadItemReplacedEvent(
item=sender.model_copy(update={"widget": updated_widget})
)
# 2. Stream assistant message
yield ThreadItemDoneEvent(
item=AssistantMessageItem(
id=self.store.generate_item_id("msg", thread, context),
thread_id=thread.id,
created_at=datetime.now(),
content=[{"text": f"Selected {line_id}"}],
)
)
# 3. Trigger client effect
yield ClientEffectEvent(
name="selection_changed",
data={"lineId": line_id},
)
4. エンティティのタグ付け (@メンション)
ユーザーがメッセージ内でエンティティを @mention できるようにします。
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: "Chat",
icon: "sparkle",
placeholderOverride: "Ask anything...",
pinned: true,
},
{
id: "event_finder",
label: "Find Events",
icon: "calendar",
placeholderOverride: "What events are you looking for?",
pinned: true,
},
];
const chatkit = useChatKit({
api: { url: API_URL, domainKey: DOMAIN_KEY },
composer: {
placeholder: "What would you like to do?",
tools: TOOL_CHOICES,
},
});
バックエンドルーティング:
async def respond(self, thread, item, context):
tool_choice = context.metadata.get("t
(原文がここで切り詰められています) 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
Building Chat Widgets
Create interactive widgets for AI chat with actions and entity tagging.
Quick Start
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}`);
}
},
},
});
Action Handler Types
| Handler | Defined In | Processed By | Use Case |
|---|---|---|---|
"client" |
Widget template | Frontend onAction |
Navigation, local state |
"server" |
Widget template | Backend action() |
Data mutation, widget replacement |
Widget Lifecycle
1. Agent tool generates widget → yield WidgetItem
2. Widget renders in chat with action buttons
3. User clicks action → action dispatched
4. Handler processes action:
- client: onAction callback in frontend
- server: action() method in ChatKitServer
5. Optional: Widget replaced with updated state
Core Patterns
1. Widget Templates
Define reusable widget layouts with dynamic data:
{
"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. Client-Handled Actions
Actions that update local state, navigate, or send follow-up messages:
Widget Definition:
{
"type": "Button",
"label": "View Article",
"onClickAction": {
"type": "open_article",
"handler": "client",
"payload": { "id": "article-123" }
}
}
Frontend Handler:
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: "More suggestions, please" });
break;
case "select_option":
setSelectedOption(action.payload?.optionId);
break;
}
},
},
});
3. Server-Handled Actions
Actions that mutate data, update widgets, or require backend processing:
Widget Definition:
{
"type": "ListViewItem",
"onClickAction": {
"type": "line.select",
"handler": "server",
"payload": { "id": "blue-line" }
}
}
Backend Handler:
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, # Note: Already RequestContext, not dict
) -> AsyncIterator[ThreadStreamEvent]:
if action.type == "line.select":
line_id = action.payload["id"] # Use .payload, not .arguments
# 1. Update widget with selection
updated_widget = build_selector_widget(selected=line_id)
yield ThreadItemReplacedEvent(
item=sender.model_copy(update={"widget": updated_widget})
)
# 2. Stream assistant message
yield ThreadItemDoneEvent(
item=AssistantMessageItem(
id=self.store.generate_item_id("msg", thread, context),
thread_id=thread.id,
created_at=datetime.now(),
content=[{"text": f"Selected {line_id}"}],
)
)
# 3. Trigger client effect
yield ClientEffectEvent(
name="selection_changed",
data={"lineId": line_id},
)
4. Entity Tagging (@mentions)
Allow users to @mention entities in messages:
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. Composer Tools (Mode Selection)
Let users select different AI modes from the composer:
const TOOL_CHOICES = [
{
id: "general",
label: "Chat",
icon: "sparkle",
placeholderOverride: "Ask anything...",
pinned: true,
},
{
id: "event_finder",
label: "Find Events",
icon: "calendar",
placeholderOverride: "What events are you looking for?",
pinned: true,
},
];
const chatkit = useChatKit({
api: { url: API_URL, domainKey: DOMAIN_KEY },
composer: {
placeholder: "What would you like to do?",
tools: TOOL_CHOICES,
},
});
Backend Routing:
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
Widget Component Reference
Layout Components
| Component | Props | Description |
|---|---|---|
ListView |
children |
Scrollable list container |
ListViewItem |
key, onClickAction, children |
Clickable list item |
Row |
gap, align, justify, children |
Horizontal flex |
Col |
gap, padding, children |
Vertical flex |
Box |
size, radius, background, padding |
Styled container |
Content Components
| Component | Props | Description |
|---|---|---|
Text |
value, size, weight, color |
Text display |
Title |
value, size, weight |
Heading text |
Image |
src, alt, width, height |
Image display |
Icon |
name, size, color |
Icon from set |
Interactive Components
| Component | Props | Description |
|---|---|---|
Button |
label, variant, onClickAction |
Clickable button |
Critical Implementation Details
Action Object Structure
IMPORTANT: Use action.payload, NOT action.arguments:
# WRONG - Will cause AttributeError
action.arguments
# CORRECT
action.payload
Context Parameter
The context parameter is RequestContext, not dict:
# WRONG - Tries to wrap RequestContext
request_context = RequestContext(metadata=context)
# CORRECT - Use directly
user_id = context.user_id
UserMessageItem Required Fields
When creating synthetic user messages:
from chatkit.types import UserMessageItem, UserMessageTextContent
# Include ALL required fields
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={},
)
Anti-Patterns
- Mixing handlers - Don't handle same action in both client and server
- Missing payload - Always include data in action payload
- Using action.arguments - Use
action.payload - Wrapping RequestContext - Context is already RequestContext
- Missing UserMessageItem fields - Include id, thread_id, created_at
- Wrong content type - Use
type="input_text"for user messages
Verification
Run: python3 scripts/verify.py
Expected: ✓ building-chat-widgets skill ready
If Verification Fails
- Check: references/ folder has widget-patterns.md
- Stop and report if still failing
References
- references/widget-patterns.md - Complete widget patterns
- references/server-action-handler.md - Backend action handling
同梱ファイル
※ ZIPに含まれるファイル一覧。`SKILL.md` 本体に加え、参考資料・サンプル・スクリプトが入っている場合があります。
- 📄 SKILL.md (9,520 bytes)
- 📎 references/server-action-handler.md (6,669 bytes)
- 📎 references/widget-patterns.md (6,300 bytes)
- 📎 scripts/verify.py (640 bytes)