building-chat-widgets by aiskillstore
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.
Content & Writing
85 Stars
2 Forks
Updated Jan 19, 2026, 04:39 AM
Why Use This
This skill provides specialized capabilities for aiskillstore's codebase.
Use Cases
- Developing new features in the aiskillstore repository
- Refactoring existing code to follow aiskillstore standards
- Understanding and working with aiskillstore's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 372 Lines
Total Files 1
Total Size 0 B
License NOASSERTION
---
name: building-chat-widgets
description: |
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.
---
# Building Chat Widgets
Create interactive widgets for AI chat with actions and entity tagging.
## Quick Start
```typescript
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:
```json
{
"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:**
```json
{
"type": "Button",
"label": "View Article",
"onClickAction": {
"type": "open_article",
"handler": "client",
"payload": { "id": "article-123" }
}
}
```
**Frontend Handler:**
```typescript
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:**
```json
{
"type": "ListViewItem",
"onClickAction": {
"type": "line.select",
"handler": "server",
"payload": { "id": "blue-line" }
}
}
```
**Backend Handler:**
```python
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:
```typescript
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:
```typescript
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:**
```python
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`:
```python
# WRONG - Will cause AttributeError
action.arguments
# CORRECT
action.payload
```
### Context Parameter
The `context` parameter is `RequestContext`, not `dict`:
```python
# 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:
```python
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
1. **Mixing handlers** - Don't handle same action in both client and server
2. **Missing payload** - Always include data in action payload
3. **Using action.arguments** - Use `action.payload`
4. **Wrapping RequestContext** - Context is already RequestContext
5. **Missing UserMessageItem fields** - Include id, thread_id, created_at
6. **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
1. Check: references/ folder has widget-patterns.md
2. **Stop and report** if still failing
## References
- [references/widget-patterns.md](references/widget-patterns.md) - Complete widget patterns
- [references/server-action-handler.md](references/server-action-handler.md) - Backend action handling Name Size