.claude/skills/building-chat-interfaces/SKILL.md
Build AI chat interfaces with custom backends, authentication, and context injection. Use when integrating chat UI with AI agents, adding auth to chat, injecting user/page context, or implementing httpOnly cookie proxies. Covers ChatKitServer, useChatKit, and MCP auth patterns. NOT when building simple chatbots without persistence or custom agent integration.
npx skillsauth add Asmayaseen/hackathon-2 building-chat-interfacesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build production-grade AI chat interfaces with custom backend integration.
# Backend (Python)
uv add chatkit-sdk agents httpx
# Frontend (React)
npm install @openai/chatkit-react
Frontend (React) Backend (Python)
┌─────────────────┐ ┌─────────────────┐
│ useChatKit() │───HTTP/SSE───>│ ChatKitServer │
│ - custom fetch │ │ - respond() │
│ - auth headers │ │ - store │
│ - page context │ │ - agent │
└─────────────────┘ └─────────────────┘
from chatkit.server import ChatKitServer
from chatkit.agents import stream_agent_response
from agents import Agent, Runner
class CustomChatKitServer(ChatKitServer[RequestContext]):
"""Extend ChatKit server with custom agent."""
async def respond(
self,
thread: ThreadMetadata,
input_user_message: UserMessageItem | None,
context: RequestContext,
) -> AsyncIterator[ThreadStreamEvent]:
if not input_user_message:
return
# Load conversation history
previous_items = await self.store.load_thread_items(
thread.id, after=None, limit=10, order="desc", context=context
)
# Build history string for prompt
history_str = "\n".join([
f"{item.role}: {item.content}"
for item in reversed(previous_items.data)
])
# Extract context from metadata
user_info = context.metadata.get('userInfo', {})
page_context = context.metadata.get('pageContext', {})
# Create agent with context in instructions
agent = Agent(
name="Assistant",
tools=[your_search_tool],
instructions=f"{history_str}\nUser: {user_info.get('name')}\n{system_prompt}",
)
# Run agent with streaming
result = Runner.run_streamed(agent, input_user_message.content)
async for event in stream_agent_response(context, result):
yield event
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
DATABASE_URL = os.getenv("DATABASE_URL").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
# Pre-warm connections on startup
async def warmup_pool():
async with engine.begin() as conn:
await conn.execute(text("SELECT 1"))
from jose import jwt
import httpx
async def get_current_user(authorization: str = Header()):
token = authorization.replace("Bearer ", "")
async with httpx.AsyncClient() as client:
jwks = (await client.get(JWKS_URL)).json()
payload = jwt.decode(token, jwks, algorithms=["RS256"])
return payload
const { control, sendUserMessage } = useChatKit({
api: {
url: `${backendUrl}/chatkit`,
domainKey: domainKey,
// Custom fetch to inject auth and context
fetch: async (url: string, options: RequestInit) => {
if (!isLoggedIn) {
throw new Error('User must be logged in');
}
const pageContext = getPageContext();
const userInfo = { id: userId, name: user.name };
// Inject metadata into request body
let modifiedOptions = { ...options };
if (modifiedOptions.body && typeof modifiedOptions.body === 'string') {
const parsed = JSON.parse(modifiedOptions.body);
if (parsed.params?.input) {
parsed.params.input.metadata = {
userId, userInfo, pageContext,
...parsed.params.input.metadata,
};
modifiedOptions.body = JSON.stringify(parsed);
}
}
return fetch(url, {
...modifiedOptions,
headers: {
...modifiedOptions.headers,
'X-User-ID': userId,
'Content-Type': 'application/json',
},
});
},
},
});
const getPageContext = useCallback(() => {
if (typeof window === 'undefined') return null;
const metaDescription = document.querySelector('meta[name="description"]')
?.getAttribute('content') || '';
const mainContent = document.querySelector('article') ||
document.querySelector('main') ||
document.body;
const headings = Array.from(mainContent.querySelectorAll('h1, h2, h3'))
.slice(0, 5)
.map(h => h.textContent?.trim())
.filter(Boolean)
.join(', ');
return {
url: window.location.href,
title: document.title,
path: window.location.pathname,
description: metaDescription,
headings: headings,
};
}, []);
const [scriptStatus, setScriptStatus] = useState<'pending' | 'ready' | 'error'>(
isBrowser && window.customElements?.get('openai-chatkit') ? 'ready' : 'pending'
);
useEffect(() => {
if (!isBrowser || scriptStatus !== 'pending') return;
if (window.customElements?.get('openai-chatkit')) {
setScriptStatus('ready');
return;
}
customElements.whenDefined('openai-chatkit').then(() => {
setScriptStatus('ready');
});
}, []);
// Only render when ready
{isOpen && scriptStatus === 'ready' && <ChatKit control={control} />}
When auth tokens are in httpOnly cookies (can't be read by JavaScript):
// app/api/chatkit/route.ts
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
export async function POST(request: NextRequest) {
const cookieStore = await cookies();
const idToken = cookieStore.get("auth_token")?.value;
if (!idToken) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
const response = await fetch(`${API_BASE}/chatkit`, {
method: "POST",
headers: {
Authorization: `Bearer ${idToken}`,
"Content-Type": "application/json",
},
body: await request.text(),
});
// Handle SSE streaming
if (response.headers.get("content-type")?.includes("text/event-stream")) {
return new Response(response.body, {
status: response.status,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
});
}
return NextResponse.json(await response.json(), { status: response.status });
}
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* MUST be beforeInteractive for web components */}
<Script
src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"
strategy="beforeInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}
MCP protocol doesn't forward auth headers. Pass credentials via system prompt:
SYSTEM_PROMPT = """You are Assistant.
## Authentication Context
- User ID: {user_id}
- Access Token: {access_token}
CRITICAL: When calling ANY MCP tool, include:
- user_id: "{user_id}"
- access_token: "{access_token}"
"""
# Format with credentials
instructions = SYSTEM_PROMPT.format(
user_id=context.user_id,
access_token=context.metadata.get("access_token", ""),
)
| Issue | Symptom | Fix | |-------|---------|-----| | History not in prompt | Agent doesn't remember conversation | Include history as string in system prompt | | Context not transmitted | Agent missing user/page info | Add to request metadata, extract in backend | | Script not loaded | Component fails to render | Detect script loading, wait before rendering | | Auth headers missing | Backend rejects requests | Use custom fetch interceptor | | httpOnly cookies | Can't read token from JS | Create server-side API route proxy | | First request slow | 7+ second delay | Pre-warm database connection pool |
Run: python3 scripts/verify.py
Expected: ✓ building-chat-interfaces skill ready
--library-id /openai/chatkit --topic useChatKitdevelopment
Systematic methodology for debugging bugs, test failures, and unexpected behavior. Use when encountering any technical issue before proposing fixes. Covers root cause investigation, pattern analysis, hypothesis testing, and fix implementation. Use ESPECIALLY when under time pressure, "just one quick fix" seems obvious, or you've already tried multiple fixes. NOT for exploratory code reading.
development
Build beautiful, accessible UIs with shadcn/ui components in Next.js. Use when creating forms, dialogs, tables, sidebars, or any UI components. Covers installation, component patterns, react-hook-form + Zod validation, and dark mode setup. NOT when building non-React applications or using different component libraries.
tools
Implement real-time streaming UI patterns for AI chat applications. Use when adding response lifecycle handlers, progress indicators, client effects, or thread state synchronization. Covers onResponseStart/End, onEffect, ProgressUpdateEvent, and client tools. NOT when building basic chat without real-time feedback.
tools
Builds AI agents using OpenAI Agents SDK with async/await patterns and multi-agent orchestration. Use when creating tutoring agents, building agent handoffs, implementing tool-calling agents, or orchestrating multiple specialists. Covers Agent class, Runner patterns, function tools, guardrails, and streaming responses. NOT when using raw OpenAI API without SDK or other agent frameworks like LangChain.