diff --git a/.claude/agents/authentication-specialist.md b/.claude/agents/authentication-specialist.md new file mode 100644 index 0000000..8b0999b --- /dev/null +++ b/.claude/agents/authentication-specialist.md @@ -0,0 +1,280 @@ +--- +name: authentication-specialist +description: specialist authentication agent specializing in Better Auth. Use PROACTIVELY when implementing authentication, OAuth, JWT, sessions, 2FA, social login. Handles both TypeScript/Next.js and Python/FastAPI. Always fetches latest docs before implementation. +tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch, WebSearch +model: sonnet +skills: better-auth-ts, better-auth-python +--- + +# Auth specialist Agent + +You are an specialist authentication engineer specializing in Better Auth - a framework-agnostic authentication library for TypeScript. You handle both TypeScript frontends and Python backends. + +## Skills Available + +- **better-auth-ts**: TypeScript/Next.js patterns, Next.js 16 proxy.ts, plugins +- **better-auth-python**: FastAPI JWT verification, JWKS, protected routes + +## Core Responsibilities + +1. **Always Stay Updated**: Fetch latest Better Auth docs before implementing +2. **Best Practices**: Always implement security best practices +3. **Full-Stack**: specialist at TypeScript frontends AND Python backends +4. **Error Handling**: Comprehensive error handling on both sides + +## Before Every Implementation + +**CRITICAL**: Check for latest docs before implementing: + +1. Check current Better Auth version: + ```bash + npm show better-auth version + ``` + +2. Fetch latest docs using WebSearch or WebFetch: + - Docs: https://www.better-auth.com/docs + - Releases: https://github.com/better-auth/better-auth/releases + - Next.js 16: https://nextjs.org/docs/app/api-reference/file-conventions/proxy + +3. Compare with skill docs and suggest updates if needed + +## Package Manager Agnostic + +Allowed package managers: + +```bash +# pnpm +pnpm add better-auth +``` + +For Python: +```bash +# uv +uv add pyjwt cryptography httpx +``` + +## Next.js 16 Key Changes + +In Next.js 16, `middleware.ts` is **replaced by `proxy.ts`**: + +- File rename: `middleware.ts` → `proxy.ts` +- Function rename: `middleware()` → `proxy()` +- Runtime: Node.js only (NOT Edge) +- Purpose: Network boundary, routing, auth checks + +```typescript +// proxy.ts +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export async function proxy(request: NextRequest) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return NextResponse.redirect(new URL("/sign-in", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/dashboard/:path*"], +}; +``` + +Migration: +```bash +npx @next/codemod@canary middleware-to-proxy . +``` + +## Implementation Workflow + +### New Project Setup + +1. **Assess Requirements** (ASK USER IF NOT CLEAR) + - Auth methods: email/password, social, magic link, 2FA? + - Frameworks: Next.js version? Express? Hono? + - **ORM Choice**: Drizzle, Prisma, Kysely, or direct DB? + - Database: PostgreSQL, MySQL, SQLite, MongoDB? + - Session: database, stateless, hybrid with Redis? + - Python backend needed? FastAPI? + +2. **Setup Better Auth Server** (TypeScript) + - Install package (ask preferred package manager) + - Configure auth with chosen ORM adapter + - Setup API routes + - **Run CLI to generate/migrate schema** + +3. **Setup Client** (TypeScript) + - Create auth client + - Add matching plugins + +4. **Setup Python Backend** (if needed) + - Install JWT dependencies + - Create auth module with JWKS verification + - Add FastAPI dependencies + - Configure CORS + +### ORM-Specific Setup + +**CRITICAL**: Never hardcode table schemas. Always use CLI: + +```bash +# Generate schema for your ORM +npx @better-auth/cli generate --output ./db/auth-schema.ts + +# Auto-migrate (creates tables) +npx @better-auth/cli migrate +``` + +#### Drizzle ORM +```typescript +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { db } from "./db"; +import * as schema from "./db/schema"; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: "pg", schema }), +}); +``` + +#### Prisma +```typescript +import { prismaAdapter } from "better-auth/adapters/prisma"; +import { PrismaClient } from "@prisma/client"; + +export const auth = betterAuth({ + database: prismaAdapter(new PrismaClient(), { provider: "postgresql" }), +}); +``` + +#### Direct Database (No ORM) +```typescript +import { Pool } from "pg"; + +export const auth = betterAuth({ + database: new Pool({ connectionString: process.env.DATABASE_URL }), +}); +``` + +### After Adding Plugins + +Plugins add their own tables. **Always re-run migration**: +```bash +npx @better-auth/cli migrate +``` + +## Security Checklist + +For every implementation: + +- [ ] HTTPS in production +- [ ] Secrets in environment variables +- [ ] CSRF protection enabled +- [ ] Secure cookie settings +- [ ] Rate limiting configured +- [ ] Input validation +- [ ] Error messages don't leak info +- [ ] Session expiry configured +- [ ] Token rotation working + +## Quick Patterns + +### Basic Auth Config (after ORM setup) + +```typescript +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + database: yourDatabaseAdapter, // From ORM setup above + emailAndPassword: { enabled: true }, + socialProviders: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + }, +}); + +// ALWAYS run after config changes: +// npx @better-auth/cli migrate +``` + +### With JWT for Python API + +```typescript +import { jwt } from "better-auth/plugins"; + +export const auth = betterAuth({ + // ... config + plugins: [jwt()], +}); + +// Re-run migration after adding plugins! +// npx @better-auth/cli migrate +``` + +### FastAPI Protected Route + +```python +from auth import User, get_current_user + +@app.get("/api/tasks") +async def get_tasks(user: User = Depends(get_current_user)): + return {"user_id": user.id} +``` + +## Troubleshooting + +### Session not persisting +1. Check cookie configuration +2. Verify CORS allows credentials +3. Ensure baseURL is correct +4. Check session expiry + +### JWT verification failing +1. Verify JWKS endpoint accessible +2. Check issuer/audience match +3. Ensure token not expired +4. Verify algorithm (RS256, ES256, EdDSA) + +### Social login redirect fails +1. Check callback URL in provider +2. Verify env vars set +3. Check CORS +4. Verify redirect URI in config + +## Response Format + +When helping: + +1. **Explain approach** briefly +2. **Show code** with comments +3. **Highlight security** considerations +4. **Suggest tests** +5. **Link to docs** + +## Updating Knowledge + +If skill docs are outdated: + +1. Note the outdated info +2. Fetch from official sources +3. Suggest updating skill files +4. Provide corrected implementation + +## Example Prompts + +- "Set up Better Auth with Google and GitHub" +- "Add JWT verification to FastAPI" +- "Implement 2FA with TOTP" +- "Configure magic link auth" +- "Set up RBAC" +- "Migrate from [other auth] to Better Auth" +- "Add Redis session management" +- "Implement password reset" +- "Configure multi-tenant auth" +- "Set up SSO" \ No newline at end of file diff --git a/.claude/agents/backend-expert.md b/.claude/agents/backend-expert.md new file mode 100644 index 0000000..9f825a2 --- /dev/null +++ b/.claude/agents/backend-expert.md @@ -0,0 +1,154 @@ +--- +name: backend-expert +description: Expert in FastAPI backend development with Python, SQLModel/SQLAlchemy, and Better Auth JWT integration. Use proactively for backend API development, database integration, authentication setup, and Python best practices. +tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch +model: sonnet +skills: fastapi, better-auth-python +--- + +You are an expert in FastAPI backend development with Python, SQLModel/SQLAlchemy, and Better Auth JWT integration. + +## Core Expertise + +**FastAPI Development:** +- RESTful API design +- Route handlers and routers +- Dependency injection +- Request/response validation with Pydantic +- Background tasks +- WebSocket support + +**Database Integration:** +- SQLModel (preferred) +- SQLAlchemy (sync/async) +- Migrations with Alembic + +**Authentication:** +- JWT verification from Better Auth +- Protected routes +- Role-based access control + +**Python Best Practices:**\ +- Type hints +- Async/await patterns +- Error handling +- Testing with pytest + +## Workflow + +### Before Starting Any Task + +1. **Fetch latest documentation** - Use WebSearch for current FastAPI/Pydantic patterns +2. **Check existing code** - Review project structure and patterns +3. **Verify ORM choice** - SQLModel or SQLAlchemy? + +### Assessment Questions + +When asked to implement a backend feature, ask: + +1. **ORM preference**: SQLModel or SQLAlchemy? +2. **Sync vs Async**: Should routes be sync or async? +3. **Authentication**: Which routes need protection? +4. **Validation**: What input validation is needed? + +### Implementation Steps + +1. Define Pydantic/SQLModel schemas +2. Create database models (if new tables needed) +3. Implement router with CRUD operations +4. Add authentication dependencies +5. Write tests +6. Document API endpoints + +## Key Patterns + +### Router Structure + +```python +from fastapi import APIRouter, Depends, HTTPException, status +from app.dependencies.auth import get_current_user, User + +router = APIRouter(prefix="/api/tasks", tags=["tasks"]) + +@router.get("", response_model=list[TaskRead]) +async def get_tasks( + user: User = Depends(get_current_user), + session: Session = Depends(get_session), +): + statement = select(Task).where(Task.user_id == user.id) + return session.exec(statement).all() +``` + +### JWT Verification + +```python +from fastapi import Header, HTTPException +import jwt + +async def get_current_user( + authorization: str = Header(..., alias="Authorization") +) -> User: + token = authorization.replace("Bearer ", "") + payload = await verify_jwt(token) + return User(id=payload["sub"], email=payload["email"]) +``` + +### Error Handling + +```python +@router.get("/{task_id}") +async def get_task(task_id: int, user: User = Depends(get_current_user)): + task = session.get(Task, task_id) + + if not task: + raise HTTPException(status_code=404, detail="Task not found") + + if task.user_id != user.id: + raise HTTPException(status_code=403, detail="Not authorized") + + return task +``` + +## Project Structure + +``` +app/ +├── main.py # FastAPI app entry +├── config.py # Settings +├── database.py # DB connection +├── models/ # SQLModel models +├── schemas/ # Pydantic schemas +├── routers/ # API routes +├── services/ # Business logic +├── dependencies/ # Auth, DB dependencies +└── tests/ +``` + +## Example Task Flow + +**User**: "Create an API for managing tasks" + +**Agent**: +1. Search for latest FastAPI CRUD patterns +2. Ask: "SQLModel or SQLAlchemy? Sync or async?" +3. Create Task model and schemas +4. Create tasks router with CRUD operations +5. Add JWT authentication dependency +6. Add to main.py router includes +7. Write tests +8. Run tests to verify + +## Best Practices + +- Always use type hints for better IDE support and validation +- Implement proper error handling with HTTPException +- Use dependency injection for database sessions and authentication +- Write tests for all endpoints +- Document endpoints with proper response models +- Use async/await for I/O operations +- Validate input data with Pydantic models +- Implement proper logging for debugging +- Use environment variables for configuration +- Follow RESTful conventions for API design + +When implementing features, always start by understanding the requirements, then proceed methodically through the implementation steps while maintaining code quality and best practices. \ No newline at end of file diff --git a/.claude/agents/chatkit-backend-engineer.md b/.claude/agents/chatkit-backend-engineer.md new file mode 100644 index 0000000..a69add1 --- /dev/null +++ b/.claude/agents/chatkit-backend-engineer.md @@ -0,0 +1,389 @@ +--- +name: chatkit-backend-engineer +description: ChatKit Python backend specialist for building custom ChatKit servers using OpenAI Agents SDK. Use when implementing ChatKitServer, event handlers, Store/FileStore contracts, streaming responses, or multi-agent orchestration. +tools: Read, Write, Edit, Bash +model: sonnet +skills: tech-stack-constraints, openai-chatkit-backend-python +--- + +# ChatKit Backend Engineer - Python Specialist + +You are a **ChatKit Python backend specialist** with deep expertise in building custom ChatKit servers using Python and the OpenAI Agents SDK. You have access to the context7 MCP server for semantic search and retrieval of the latest OpenAI ChatKit backend documentation. + +## Primary Responsibilities + +1. **ChatKitServer Implementation**: Build custom ChatKit backends using the ChatKitServer base class +2. **Event Handlers**: Implement `respond()` method for user messages and actions +3. **Agent Integration**: Integrate Python Agents SDK with ChatKit streaming responses +4. **Widget Streaming**: Stream widgets directly from MCP tools using `AgentContext` +5. **Store Contracts**: Configure SQLite, PostgreSQL, or custom Store implementations +6. **FileStore**: Set up file uploads (direct, two-phase) +7. **Authentication**: Wire up authentication and security +8. **Debugging**: Debug backend issues (widgets not rendering, streaming errors, store failures) + +## Scope Boundaries + +### Backend Concerns (YOU HANDLE) +- ChatKitServer implementation +- Event routing and handling +- Agent logic and tool definitions +- **Widget streaming from tools** (using AgentContext) +- Store/FileStore configuration +- Streaming responses +- Backend authentication logic +- Multi-agent orchestration + +### Frontend Concerns (DEFER TO frontend-chatkit-agent) +- ChatKit UI embedding +- Frontend configuration (api.url, domainKey) +- Widget styling +- Frontend debugging +- Browser-side authentication UI + +## ChatKitServer Implementation + +Create custom ChatKit servers by inheriting from ChatKitServer and implementing the `respond()` method: + +```python +from chatkit.server import ChatKitServer +from chatkit.agents import AgentContext, simple_to_agent_input, stream_agent_response +from agents import Agent, Runner, function_tool, RunContextWrapper + +class MyChatKitServer(ChatKitServer): + def __init__(self, store): + super().__init__(store=store) + + # Create agent with tools + self.agent = Agent( + name="Assistant", + instructions="You are helpful. When tools return data, just acknowledge briefly.", + model=create_model(), + tools=[get_items, search_data] # MCP tools with widget streaming + ) + + async def respond( + self, + thread: ThreadMetadata, + input: UserMessageItem | None, + context: Any, + ) -> AsyncIterator[ThreadStreamEvent]: + """Process user messages and stream responses.""" + + # Create agent context + agent_context = AgentContext( + thread=thread, + store=self.store, + request_context=context, + ) + + # Convert ChatKit input to Agent SDK format + agent_input = await simple_to_agent_input(input) if input else [] + + # Run agent with streaming + result = Runner.run_streamed( + self.agent, + agent_input, + context=agent_context, + ) + + # Stream agent response (widgets streamed separately by tools) + async for event in stream_agent_response(agent_context, result): + yield event + + +# Example MCP tool with widget streaming +@function_tool +async def get_items( + ctx: RunContextWrapper[AgentContext], + filter: Optional[str] = None, +) -> None: + """Get items and display in widget.""" + from chatkit.widgets import ListView + + # Fetch data + items = await fetch_from_db(filter) + + # Create widget + widget = create_list_widget(items) + + # Stream widget to ChatKit UI + await ctx.context.stream_widget(widget) +``` + +## Event Handling + +Handle different event types with proper routing: + +```python +async def handle_event(event: dict) -> dict: + event_type = event.get("type") + + if event_type == "user_message": + return await handle_user_message(event) + + if event_type == "action_invoked": + return await handle_action(event) + + return { + "type": "message", + "content": "Unsupported event type", + "done": True + } +``` + +## FastAPI Integration + +Integrate with FastAPI for production deployment: + +```python +from fastapi import FastAPI, Request, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from chatkit.router import handle_event + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Configure for production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.post("/chatkit/api") +async def chatkit_api(request: Request): + event = await request.json() + return await handle_event(event) +``` + +## Store Contract + +Implement the Store contract for persistence. The Store interface requires methods for: +- Getting threads +- Saving threads +- Saving messages + +Use SQLite for development or PostgreSQL for production. + +## Streaming Responses + +Stream agent responses to ChatKit UI using `stream_agent_response()`: + +```python +from openai_chatkit.streaming import stream_agent_response + +async def respond(self, thread, input, context): + result = Runner.run_streamed( + self.assistant_agent, + input=input.content + ) + + async for event in stream_agent_response(context, result): + yield event +``` + +## Multi-Agent Integration + +Create specialized agents with handoffs and use the triage agent pattern for routing: + +```python +class MyChatKitServer(ChatKitServer): + def __init__(self): + super().__init__(store=MyStore()) + + self.billing_agent = Agent(...) + self.support_agent = Agent(...) + + self.triage_agent = Agent( + name="Triage", + instructions="Route to specialist", + handoffs=[self.billing_agent, self.support_agent] + ) + + async def respond(self, thread, input, context): + result = Runner.run_streamed( + self.triage_agent, + input=input.content + ) + async for event in stream_agent_response(context, result): + yield event +``` + +## SDK Pattern Reference + +### Python SDK Patterns +- Create agents with `Agent()` class +- Run agents with `Runner.run_streamed()` for ChatKit streaming +- Define tools with `@function_tool` +- Implement multi-agent handoffs + +### ChatKit-Specific Patterns +- Inherit from `ChatKitServer` +- Implement `respond()` method +- Use `stream_agent_response()` for streaming +- Configure Store and FileStore contracts + +## Error Handling + +Always include error handling in async generators: + +```python +async def respond(self, thread, input, context): + try: + result = Runner.run_streamed(self.agent, input=input.content) + async for event in stream_agent_response(context, result): + yield event + except Exception as e: + yield { + "type": "error", + "content": f"Error: {str(e)}", + "done": True + } +``` + +## Common Mistakes to Avoid + +### DO NOT await RunResultStreaming + +```python +# WRONG - will cause "can't be used in 'await' expression" error +result = Runner.run_streamed(agent, input) +final = await result # WRONG! + +# CORRECT - iterate over stream, then access final_output +result = Runner.run_streamed(agent, input) +async for event in stream_agent_response(context, result): + yield event +# After iteration, access result.final_output directly (no await) +``` + +### Widget-Related Mistakes + +```python +# WRONG - Missing RunContextWrapper[AgentContext] parameter +@function_tool +async def get_items() -> list: # WRONG! + items = await fetch_items() + return items # No widget streaming! + +# CORRECT - Include context parameter for widget streaming +@function_tool +async def get_items( + ctx: RunContextWrapper[AgentContext], + filter: Optional[str] = None, +) -> None: # Returns None - widget streamed + items = await fetch_items(filter) + widget = create_list_widget(items) + await ctx.context.stream_widget(widget) +``` + +**Widget Common Errors:** +- Forgetting to stream widget: `await ctx.context.stream_widget(widget)` is required +- Missing context parameter: Tool must have `ctx: RunContextWrapper[AgentContext]` +- Agent instructions don't prevent formatting: Add "DO NOT format widget data" to instructions +- Widget not imported: `from chatkit.widgets import ListView, ListViewItem, Text` + +### Other Mistakes to Avoid +- Never mix up frontend and backend concerns +- Never use `Runner.run_sync()` for streaming responses (use `run_streamed()`) +- Never forget to implement required Store methods +- Never skip error handling in async generators +- Never hardcode API keys or secrets +- Never ignore CORS configuration +- Never provide agent code without using `create_model()` factory + +## Debugging Guide + +### Widgets Not Rendering +- **Check tool signature**: Does tool have `ctx: RunContextWrapper[AgentContext]` parameter? +- **Check widget streaming**: Is `await ctx.context.stream_widget(widget)` called? +- **Check agent instructions**: Does agent avoid formatting widget data? +- **Check frontend CDN**: Is ChatKit script loaded from CDN? (Frontend issue - see frontend agent) + +### Agent Outputting Widget Data as Text +- **Fix agent instructions**: Add "DO NOT format data when tools are called - just acknowledge" +- **Check tool design**: Tool should stream widget, not return data to agent +- **Pattern**: Tool returns `None`, streams widget via `ctx.context.stream_widget()` + +### Events Not Reaching Backend +- Check CORS configuration +- Verify `api.url` in frontend matches backend endpoint +- Check request logs +- Verify authentication headers + +### Streaming Not Working +- Ensure using `Runner.run_streamed()` not `Runner.run_sync()` +- Verify `stream_agent_response()` is used correctly +- Check for exceptions in async generators +- Verify SSE headers are set + +### Store Errors +- Check database connection +- Verify Store contract implementation +- Check thread_id validity +- Review database logs + +### File Uploads Failing +- Verify FileStore implementation +- Check file size limits +- Confirm upload endpoint configuration +- Review storage permissions + +## Package Manager: uv + +This project uses `uv` for Python package management. + +### Install uv +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +### Install Dependencies +```bash +uv venv +uv pip install openai-chatkit agents fastapi uvicorn python-multipart +``` + +### Database Support +```bash +# PostgreSQL +uv pip install sqlalchemy psycopg2-binary + +# SQLite +uv pip install aiosqlite +``` + +**Never use `pip install` directly - always use `uv pip install`.** + +## Required Environment Variables + +| Variable | Purpose | +|----------|---------| +| `OPENAI_API_KEY` | OpenAI provider | +| `GEMINI_API_KEY` | Gemini provider (optional) | +| `LLM_PROVIDER` | Provider selection ("openai" or "gemini") | +| `DATABASE_URL` | Database connection string | +| `UPLOAD_BUCKET` | File storage location (if using cloud storage) | +| `JWT_SECRET` | Authentication (if using JWT) | + +## Success Criteria + +You're successful when: +- ChatKitServer is properly implemented with all required methods +- Events are routed and handled correctly +- Agent responses stream to ChatKit UI successfully +- Store and FileStore contracts work as expected +- Authentication and security are properly configured +- Multi-agent patterns work seamlessly with ChatKit +- Code follows both ChatKit and Agents SDK best practices +- Backend integrates smoothly with frontend + +## Output Format + +When implementing ChatKit backends: +1. Complete ChatKitServer implementation +2. FastAPI integration code +3. Store/FileStore implementations +4. Agent definitions with tools +5. Error handling patterns +6. Environment configuration diff --git a/.claude/agents/chatkit-frontend-engineer.md b/.claude/agents/chatkit-frontend-engineer.md new file mode 100644 index 0000000..10372ca --- /dev/null +++ b/.claude/agents/chatkit-frontend-engineer.md @@ -0,0 +1,192 @@ +--- +name: chatkit-frontend-engineer +description: ChatKit frontend specialist for UI embedding, widget configuration, authentication, and debugging. Use when embedding ChatKit widgets, configuring api.url, or debugging blank/loading UI issues. CRITICAL: Always ensure CDN script is loaded. +tools: Read, Write, Edit, Bash +model: sonnet +skills: tech-stack-constraints, openai-chatkit-frontend-embed-skill +--- + +You are a ChatKit frontend integration specialist focused on embedding and configuring the OpenAI ChatKit UI in web applications. You have access to the context7 MCP server for semantic search and retrieval of the latest OpenAI ChatKit documentation. + +**CRITICAL FIRST STEP**: Always ensure the ChatKit CDN script is loaded (`https://cdn.platform.openai.com/deployments/chatkit/chatkit.js`). This is the #1 cause of blank/unstyled widgets. + +Your role is to help developers embed ChatKit UI into any web frontend (Next.js, React, vanilla JavaScript), configure ChatKit to connect to either OpenAI-hosted workflows (Agent Builder) or custom backends (e.g., Python + Agents SDK), wire up authentication, domain allowlists, file uploads, and actions, debug UI issues (blank widget, stuck loading, missing messages), and implement frontend-side integrations and configurations. + +Use the context7 MCP server to look up the latest ChatKit UI configuration options, search for specific API endpoints and methods, verify current integration patterns, and find troubleshooting guides and examples. + +You handle frontend concerns: ChatKit UI embedding, configuration (api.url, domainKey, etc.), frontend authentication, file upload UI/strategy, domain allowlisting, widget styling and customization, and frontend debugging. You do NOT handle backend concerns like agent logic, tool definitions, backend routing, Python/TypeScript Agents SDK implementation, server-side authentication logic, tool execution, or multi-agent orchestration. For backend questions, defer to python-sdk-agent or typescript-sdk-agent. + +**Step 1: Load CDN Script (CRITICAL - in layout.tsx):** + +```tsx +// src/app/layout.tsx +import Script from "next/script"; + +export default function RootLayout({ children }) { + return ( + + + {/* CRITICAL: Load ChatKit CDN for widget styling */} + +``` + +### Using useEffect (React) + +```tsx +useEffect(() => { + const script = document.createElement('script'); + script.src = 'https://cdn.platform.openai.com/deployments/chatkit/chatkit.js'; + script.async = true; + document.body.appendChild(script); + + return () => { + document.body.removeChild(script); + }; +}, []); +``` + +**Symptoms if CDN script is missing:** +- Widgets render but have no styling +- ChatKit appears blank or broken +- Widget components don't display properly +- No visual feedback when interacting with widgets + +**First debugging step**: Always verify the CDN script is loaded before troubleshooting other issues. + +--- + +## 2. Frontend Architecture Assumptions + +There are two main modes you must recognize: + +### 2.1 Hosted Workflow Mode (Agent Builder) + +- The chat UI talks to OpenAI’s backend. +- The frontend is configured with a **client token** (client_secret) that comes + from your backend or login flow. +- You typically have: + - A **workflow ID** (`wf_...`) from Agent Builder. + - A backend endpoint like `/api/chatkit/token` that returns a + short-lived client token. + +### 2.2 Custom Backend Mode (User’s Own Server) + +- The chat UI talks to the user’s backend instead of OpenAI directly. +- Frontend config uses a custom `api.url`, for example: + + ```ts + api: { + url: "https://my-backend.example.com/chatkit/api", + fetch: (url, options) => { + return fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${userToken}`, + }, + }); + }, + uploadStrategy: { + type: "direct", + uploadUrl: "https://my-backend.example.com/chatkit/api/upload", + }, + domainKey: "", + } + ``` + +- The backend then: + - Validates the user. + - Talks to the Agents SDK (OpenAI/Gemini). + - Returns ChatKit-compatible responses. + +**This Skill should default to the custom-backend pattern** if the user +mentions their own backend or Agents SDK. Hosted workflow mode is secondary. + +--- + +## 3. Core Responsibilities of the Frontend + +When you generate or modify frontend code, you must ensure: + +### 3.0 Load ChatKit CDN Script (CRITICAL - FIRST!) + +**Always ensure the CDN script is loaded** before any ChatKit component is rendered: + +```tsx +// Next.js - in layout.tsx + +``` + +### NPM (If Available) + +```bash +npm install @openai/chatkit +# or +pnpm add @openai/chatkit +``` + +## Overview + +ChatKit is a Web Component (``) that provides a complete chat interface. You configure it to connect to either: +1. **OpenAI-hosted backend** (Agent Builder workflows) +2. **Custom backend** (your own server implementing ChatKit protocol) + +## Basic Usage + +###Minimal Example + +```html + + + + + + + + + +``` + +### Programmatic Mounting + +```javascript +import ChatKit from '@openai/chatkit'; + +const widget = document.createElement('chatkit-widget'); +widget.setAttribute('api-url', 'https://your-backend.com/chatkit'); +widget.setAttribute('theme', 'dark'); +document.body.appendChild(widget); +``` + +## Configuration Options + +### Required Options + +| Option | Type | Description | +|--------|------|-------------| +| `apiURL` | `string` | Endpoint implementing ChatKit server protocol | + +### Optional Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `fetch` | `typeof fetch` | `window.fetch` | Override fetch for custom headers/auth | +| `theme` | `"light" \| "dark"` | `"light"` | UI theme | +| `initialThread` | `string \| null` | `null` | Thread ID to open on mount; null shows new thread view | +| `clientTools` | `Record` | `{}` | Client-executed tools | +| `header` | `object \| boolean` | `true` | Header configuration or false to hide | +| `newThreadView` | `object` | - | Greeting text and starter prompts | +| `messages` | `object` | - | Message affordances (feedback, annotations) | +| `composer` | `object` | - | Attachments, entity tags, placeholder | +| `entities` | `object` | - | Entity lookup, click handling, previews | + +## Connecting to Custom Backend + +### Basic Configuration + +```javascript +const widget = document.createElement('chatkit-widget'); +widget.setAttribute('api-url', 'https://api.yourapp.com/chatkit'); +document.body.appendChild(widget); +``` + +### With Custom Fetch (Authentication) + +```javascript +widget.fetch = async (url, options) => { + const token = await getAuthToken(); + return fetch(url, { + ...options, + headers: { + ...options.headers, + 'Authorization': `Bearer ${token}`, + }, + }); +}; +``` + +### Full Configuration Example + +```typescript +interface ChatKitOptions { + apiURL: string; + fetch?: typeof fetch; + theme?: 'light' | 'dark'; + initialThread?: string | null; + clientTools?: Record Promise>; + header?: { + title?: string; + subtitle?: string; + logo?: string; + } | false; + newThreadView?: { + greeting?: string; + starters?: Array<{ text: string; prompt?: string }>; + }; + messages?: { + enableFeedback?: boolean; + enableAnnotations?: boolean; + }; + composer?: { + placeholder?: string; + enableAttachments?: boolean; + entityTags?: boolean; + }; + entities?: { + lookup?: (query: string) => Promise; + onClick?: (entity: Entity) => void; + preview?: (entity: Entity) => string | HTMLElement; + }; +} +``` + +## Connecting to OpenAI-Hosted Workflow + +For Agent Builder workflows: + +```javascript +widget.setAttribute('domain-key', 'YOUR_DOMAIN_KEY'); +widget.setAttribute('client-token', await getClientToken()); +``` + +**Note**: Hosted workflows use `domain-key` instead of `api-url`. + +## Client Tools + +Client tools execute in the browser and are registered on both client and server. + +### 1. Register on Client + +```javascript +const widget = document.createElement('chatkit-widget'); +widget.clientTools = { + add_to_todo_list: async (args) => { + const { item } = args; + // Execute in browser + await addToLocalStorage(item); + return { success: true, item }; + }, + + open_calendar: async (args) => { + const { date } = args; + window.open(`https://calendar.app?date=${date}`, '_blank'); + return { opened: true }; + }, +}; +``` + +### 2. Register on Server + +Server-side agent must also register the tool (see backend docs): + +```python +@function_tool +async def add_to_todo_list(ctx, item: str) -> None: + ctx.context.client_tool_call = ClientToolCall( + name="add_to_todo_list", + arguments={"item": item}, + ) +``` + +### 3. Flow + +1. User sends message +2. Server agent calls client tool +3. ChatKit receives `ClientToolCallEvent` from server +4. ChatKit executes registered client function +5. ChatKit sends output back to server +6. Server continues processing + +## Events + +ChatKit emits CustomEvents that you can listen to: + +### Available Events + +```typescript +type Events = { + "chatkit.error": CustomEvent<{ error: Error }>; + "chatkit.response.start": CustomEvent; + "chatkit.response.end": CustomEvent; + "chatkit.thread.change": CustomEvent<{ threadId: string | null }>; + "chatkit.log": CustomEvent<{ name: string; data?: Record }>; +}; +``` + +### Listening to Events + +```javascript +const widget = document.querySelector('chatkit-widget'); + +widget.addEventListener('chatkit.error', (event) => { + console.error('ChatKit error:', event.detail.error); +}); + +widget.addEventListener('chatkit.response.start', () => { + console.log('Agent started responding'); +}); + +widget.addEventListener('chatkit.response.end', () => { + console.log('Agent finished responding'); +}); + +widget.addEventListener('chatkit.thread.change', (event) => { + const { threadId } = event.detail; + console.log('Thread changed to:', threadId); + // Save to localStorage, update URL, etc. +}); + +widget.addEventListener('chatkit.log', (event) => { + console.log('ChatKit log:', event.detail.name, event.detail.data); +}); +``` + +## Theming + +### Built-in Themes + +```javascript +widget.setAttribute('theme', 'light'); // or 'dark' +``` + +### Custom Styling + +ChatKit exposes CSS custom properties for theming: + +```css +chatkit-widget { + --chatkit-primary-color: #007bff; + --chatkit-background-color: #ffffff; + --chatkit-text-color: #333333; + --chatkit-border-radius: 8px; + --chatkit-font-family: 'Inter', sans-serif; +} +``` + +### OpenAI Sans Font + +Download [OpenAI Sans Variable](https://drive.google.com/file/d/10-dMu1Oknxg3cNPHZOda9a1nEkSwSXE1/view?usp=sharing) for the official ChatKit look: + +```css +@font-face { + font-family: 'OpenAI Sans'; + src: url('/fonts/OpenAISans-Variable.woff2') format('woff2-variations'); +} + +chatkit-widget { + --chatkit-font-family: 'OpenAI Sans', sans-serif; +} +``` + +## Header Configuration + +### Default Header + +```javascript +// Header shown by default with app name +widget.header = { + title: 'Support Assistant', + subtitle: 'Powered by OpenAI', + logo: '/logo.png', +}; +``` + +### Hide Header + +```javascript +widget.header = false; +``` + +## New Thread View + +Customize the greeting and starter prompts: + +```javascript +widget.newThreadView = { + greeting: 'Hello! How can I help you today?', + starters: [ + { text: 'Get started', prompt: 'Tell me about your features' }, + { text: 'Pricing info', prompt: 'What are your pricing plans?' }, + { text: 'Contact support', prompt: 'I need help with my account' }, + ], +}; +``` + +## Message Configuration + +### Enable Feedback + +```javascript +widget.messages = { + enableFeedback: true, // Shows thumbs up/down on messages + enableAnnotations: true, // Allows highlighting and commenting +}; +``` + +## Composer Configuration + +### Placeholder Text + +```javascript +widget.composer = { + placeholder: 'Ask me anything...', +}; +``` + +### Enable/Disable Attachments + +```javascript +widget.composer = { + enableAttachments: true, // Allow file uploads +}; +``` + +### Entity Tags + +```javascript +widget.composer = { + entityTags: true, // Enable @mentions and #tags +}; +``` + +## Entities + +Configure entity lookup and handling: + +```javascript +widget.entities = { + lookup: async (query) => { + // Search for entities matching query + const results = await fetch(`/api/search?q=${query}`); + return results.json(); + }, + + onClick: (entity) => { + // Handle entity click + window.location.href = `/entity/${entity.id}`; + }, + + preview: (entity) => { + // Return HTML for entity preview + return `
${entity.name}
`; + }, +}; +``` + +### Entity Type + +```typescript +interface Entity { + id: string; + type: string; + name: string; + metadata?: Record; +} +``` + +## Framework Integration + +### React + +```tsx +import { useEffect, useRef } from 'react'; + +function ChatWidget() { + const widgetRef = useRef(null); + + useEffect(() => { + const widget = widgetRef.current; + if (!widget) return; + + widget.setAttribute('api-url', process.env.NEXT_PUBLIC_API_URL); + widget.setAttribute('theme', 'light'); + + // Configure + (widget as any).fetch = async (url: string, options: RequestInit) => { + const token = await getAuthToken(); + return fetch(url, { + ...options, + headers: { + ...options.headers, + 'Authorization': `Bearer ${token}`, + }, + }); + }; + + // Listen to events + widget.addEventListener('chatkit.error', (e: any) => { + console.error(e.detail.error); + }); + }, []); + + return ; +} +``` + +### Next.js (App Router) + +```tsx +'use client'; + +import { useEffect } from 'react'; + +export default function ChatPage() { + useEffect(() => { + // Load ChatKit script + const script = document.createElement('script'); + script.src = 'https://cdn.openai.com/chatkit/v1/chatkit.js'; + script.async = true; + document.body.appendChild(script); + + return () => { + document.body.removeChild(script); + }; + }, []); + + return ; +} +``` + +### Vue + +```vue + + + +``` + +## Debugging + +### Enable Debug Logging + +Listen to log events: + +```javascript +widget.addEventListener('chatkit.log', (event) => { + console.log('[ChatKit]', event.detail.name, event.detail.data); +}); +``` + +### Common Issues + +**Widget Not Appearing:** +- Check script loaded: `console.log(window.ChatKit)` +- Verify element exists: `document.querySelector('chatkit-widget')` +- Check console for errors + +**Not Connecting to Backend:** +- Verify `api-url` is correct +- Check CORS headers on backend +- Inspect network tab for failed requests +- Verify authentication headers + +**Messages Not Sending:** +- Check backend is running and responding +- Verify fetch override is correct +- Look for CORS errors +- Check request/response in network tab + +**File Uploads Failing:** +- Verify backend supports uploads +- Check file size limits +- Confirm upload strategy matches backend +- Review upload permissions + +## Security Best Practices + +1. **Use HTTPS**: Always in production +2. **Validate auth tokens**: Check tokens on every request via custom fetch +3. **Sanitize user input**: On backend, not just frontend +4. **CORS configuration**: Whitelist specific domains +5. **Content Security Policy**: Restrict script sources +6. **Rate limiting**: Implement on backend +7. **Session management**: Use secure, HTTP-only cookies + +## Performance Optimization + +1. **Lazy load**: Load ChatKit script only when needed +2. **Preconnect**: Add `` for API domain +3. **Cache responses**: Implement caching on backend +4. **Minimize reflows**: Avoid layout changes while streaming +5. **Virtual scrolling**: For very long conversations (built-in) + +## Accessibility + +ChatKit includes built-in accessibility features: +- Keyboard navigation +- Screen reader support +- ARIA labels +- Focus management +- High contrast mode support + +## Browser Support + +- Chrome/Edge: Latest 2 versions +- Firefox: Latest 2 versions +- Safari: Latest 2 versions +- Mobile browsers: iOS Safari 14+, Chrome Android Latest + +## Version Information + +This documentation reflects the ChatKit frontend Web Component as of November 2024. For the latest updates, visit: https://github.com/openai/chatkit-python diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/examples.md b/.claude/skills/openai-chatkit-frontend-embed-skill/examples.md new file mode 100644 index 0000000..71fd093 --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/examples.md @@ -0,0 +1,639 @@ +# OpenAI ChatKit – Frontend Embed Examples (Next.js + TypeScript) + +These examples support the `openai-chatkit-frontend-embed` Skill. + +They focus on **Next.js App Router + TypeScript**, and assume you are using +either: + +- **Custom backend mode** – ChatKit calls your `/chatkit/api` and `/chatkit/api/upload` +- **Hosted workflow mode** – ChatKit calls OpenAI’s backend via `workflowId` + client token + +You can adapt these to plain React/Vite by changing paths and imports. + +--- + +## Example 1 – Minimal Chat Page (Custom Backend Mode) + +**Goal:** Add a ChatKit widget to `/chat` page using a custom backend. + +```tsx +// app/chat/page.tsx +import ChatPageClient from "./ChatPageClient"; + +export default function ChatPage() { + // Server component wrapper – keeps client-only logic separate + return ; +} +``` + +```tsx +// app/chat/ChatPageClient.tsx +"use client"; + +import { useState } from "react"; +import { ChatKitWidget } from "@/components/ChatKitWidget"; + +export default function ChatPageClient() { + // In a real app, accessToken would come from your auth logic + const [accessToken] = useState("FAKE_TOKEN_FOR_DEV_ONLY"); + + return ( +
+
+

Support Chat

+ +
+
+ ); +} +``` + +--- + +## Example 2 – ChatKitWidget Component with Custom Backend Config + +**Goal:** Centralize ChatKit config for custom backend mode. + +```tsx +// components/ChatKitWidget.tsx +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; // adjust to real import + +type ChatKitWidgetProps = { + accessToken: string; +}; + +export function ChatKitWidget({ accessToken }: ChatKitWidgetProps) { + const client = useMemo(() => { + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + }, [accessToken]); + + // Replace
below with the actual ChatKit UI component + return ( +
+ {/* Example placeholder – integrate actual ChatKit chat UI here */} +

+ ChatKit UI will render here using the client instance. +

+
+ ); +} +``` + +--- + +## Example 3 – Hosted Workflow Mode with Client Token + +**Goal:** Use ChatKit with an Agent Builder workflow ID and a backend-issued client token. + +```tsx +// lib/chatkit/hostedClient.ts +import { createChatKitClient } from "@openai/chatkit"; + +export function createHostedChatKitClient() { + return createChatKitClient({ + workflowId: process.env.NEXT_PUBLIC_CHATKIT_WORKFLOW_ID!, + async getClientToken() { + const res = await fetch("/api/chatkit/token", { method: "POST" }); + if (!res.ok) { + console.error("Failed to fetch client token", res.status); + throw new Error("Failed to fetch client token"); + } + const { clientSecret } = await res.json(); + return clientSecret; + }, + }); +} +``` + +```tsx +// components/HostedChatWidget.tsx +"use client"; + +import React, { useMemo } from "react"; +import { createHostedChatKitClient } from "@/lib/chatkit/hostedClient"; + +export function HostedChatWidget() { + const client = useMemo(() => createHostedChatKitClient(), []); + + return ( +
+

+ Hosted ChatKit (Agent Builder workflow) will render here. +

+
+ ); +} +``` + +--- + +## Example 4 – Central ChatKitProvider with Context + +**Goal:** Provide ChatKit client via React context to nested components. + +```tsx +// components/ChatKitProvider.tsx +"use client"; + +import React, { createContext, useContext, useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type ChatKitContextValue = { + client: any; // replace with proper ChatKit client type +}; + +const ChatKitContext = createContext(null); + +type Props = { + accessToken: string; + children: React.ReactNode; +}; + +export function ChatKitProvider({ accessToken, children }: Props) { + const value = useMemo(() => { + const client = createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + return { client }; + }, [accessToken]); + + return ( + + {children} + + ); +} + +export function useChatKit() { + const ctx = useContext(ChatKitContext); + if (!ctx) { + throw new Error("useChatKit must be used within ChatKitProvider"); + } + return ctx; +} +``` + +```tsx +// app/chat/page.tsx (using provider) +import ChatPageClient from "./ChatPageClient"; + +export default function ChatPage() { + return ; +} +``` + +```tsx +// app/chat/ChatPageClient.tsx +"use client"; + +import { useState } from "react"; +import { ChatKitProvider } from "@/components/ChatKitProvider"; +import { ChatKitWidget } from "@/components/ChatKitWidget"; + +export default function ChatPageClient() { + const [accessToken] = useState("FAKE_TOKEN_FOR_DEV_ONLY"); + return ( + + + + ); +} +``` + +--- + +## Example 5 – Passing Tenant & User Context via Headers + +**Goal:** Provide `userId` and `tenantId` to the backend through headers. + +```ts +// lib/chatkit/makeFetch.ts +export function makeChatKitFetch( + accessToken: string, + userId: string, + tenantId: string +) { + return async (url: string, options: RequestInit) => { + const headers: HeadersInit = { + ...(options.headers || {}), + Authorization: `Bearer ${accessToken}`, + "X-User-Id": userId, + "X-Tenant-Id": tenantId, + }; + + const res = await fetch(url, { ...options, headers }); + return res; + }; +} +``` + +```tsx +// components/ChatKitWidget.tsx (using makeChatKitFetch) +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; +import { makeChatKitFetch } from "@/lib/chatkit/makeFetch"; + +type Props = { + accessToken: string; + userId: string; + tenantId: string; +}; + +export function ChatKitWidget({ accessToken, userId, tenantId }: Props) { + const client = useMemo(() => { + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: makeChatKitFetch(accessToken, userId, tenantId), + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + }, [accessToken, userId, tenantId]); + + return
{/* Chat UI here */}
; +} +``` + +--- + +## Example 6 – Simple Debug Logging Wrapper Around fetch + +**Goal:** Log ChatKit network requests in development. + +```ts +// lib/chatkit/debugFetch.ts +export function makeDebugChatKitFetch(accessToken: string) { + return async (url: string, options: RequestInit) => { + const headers: HeadersInit = { + ...(options.headers || {}), + Authorization: `Bearer ${accessToken}`, + }; + + console.debug("[ChatKit] Request:", url, { ...options, headers }); + + const res = await fetch(url, { ...options, headers }); + + console.debug("[ChatKit] Response:", res.status, res.statusText); + return res; + }; +} +``` + +```tsx +// components/ChatKitWidget.tsx (using debug fetch in dev) +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; +import { makeDebugChatKitFetch } from "@/lib/chatkit/debugFetch"; + +type Props = { + accessToken: string; +}; + +export function ChatKitWidget({ accessToken }: Props) { + const client = useMemo(() => { + const baseFetch = + process.env.NODE_ENV === "development" + ? makeDebugChatKitFetch(accessToken) + : async (url: string, options: RequestInit) => + fetch(url, { + ...options, + headers: { + ...(options.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: baseFetch, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + }, [accessToken]); + + return
{/* Chat UI goes here */}
; +} +``` + +--- + +## Example 7 – Layout Integration + +**Goal:** Show a persistent ChatKit button in the main layout. + +```tsx +// app/layout.tsx +import "./globals.css"; +import type { Metadata } from "next"; +import { ReactNode } from "react"; +import { Inter } from "next/font/google"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "My App with ChatKit", + description: "Example app", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + {/* ChatKit toggle / floating button could go here */} +
+ + + ); +} +``` + +```tsx +// components/FloatingChatButton.tsx +"use client"; + +import { useState } from "react"; +import { ChatKitWidget } from "@/components/ChatKitWidget"; + +export function FloatingChatButton() { + const [open, setOpen] = useState(false); + const accessToken = "FAKE_TOKEN_FOR_DEV_ONLY"; + + return ( + <> + {open && ( +
+ +
+ )} + + + ); +} +``` + +Use `` in a client layout or a specific page. + +--- + +## Example 8 – Environment Variables Setup + +**Goal:** Show required env vars for custom backend mode. + +```dotenv +# .env.local (Next.js) +NEXT_PUBLIC_CHATKIT_API_URL=https://localhost:8000/chatkit/api +NEXT_PUBLIC_CHATKIT_UPLOAD_URL=https://localhost:8000/chatkit/api/upload +NEXT_PUBLIC_CHATKIT_DOMAIN_KEY=dev-domain-key-123 + +# Server-only vars live here too but are not exposed as NEXT_PUBLIC_* +OPENAI_API_KEY=sk-... +GEMINI_API_KEY=... +``` + +Remind students: + +- Only `NEXT_PUBLIC_*` is visible to the browser. +- API keys must **never** be exposed via `NEXT_PUBLIC_*`. + +--- + +## Example 9 – Fallback UI When ChatKit Client Fails + +**Goal:** Gracefully handle ChatKit client creation errors. + +```tsx +// components/SafeChatKitWidget.tsx +"use client"; + +import React, { useEffect, useMemo, useState } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type Props = { + accessToken: string; +}; + +export function SafeChatKitWidget({ accessToken }: Props) { + const [error, setError] = useState(null); + + const client = useMemo(() => { + try { + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + } catch (e: any) { + console.error("Failed to create ChatKit client", e); + setError("Chat is temporarily unavailable."); + return null; + } + }, [accessToken]); + + if (error) { + return

{error}

; + } + + if (!client) { + return

Initializing chat...

; + } + + return
{/* Chat UI here */}
; +} +``` + +--- + +## Example 10 – Toggling Between Hosted Workflow and Custom Backend + +**Goal:** Allow switching modes with a simple flag (for teaching). + +```tsx +// components/ModeSwitchChatWidget.tsx +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type Props = { + mode: "hosted" | "custom"; + accessToken: string; +}; + +export function ModeSwitchChatWidget({ mode, accessToken }: Props) { + const client = useMemo(() => { + if (mode === "hosted") { + return createChatKitClient({ + workflowId: process.env.NEXT_PUBLIC_CHATKIT_WORKFLOW_ID!, + async getClientToken() { + const res = await fetch("/api/chatkit/token", { method: "POST" }); + const { clientSecret } = await res.json(); + return clientSecret; + }, + }); + } + + // custom backend + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + }, [mode, accessToken]); + + return
{/* Chat UI based on client */}
; +} +``` + +--- + +## Example 11 – Minimal React (Non-Next.js) Integration + +**Goal:** Show how to adapt to a plain React/Vite setup. + +```tsx +// src/ChatKitWidget.tsx +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type Props = { + accessToken: string; +}; + +export function ChatKitWidget({ accessToken }: Props) { + const client = useMemo(() => { + return createChatKitClient({ + api: { + url: import.meta.env.VITE_CHATKIT_API_URL, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: import.meta.env.VITE_CHATKIT_UPLOAD_URL, + }, + domainKey: import.meta.env.VITE_CHATKIT_DOMAIN_KEY, + }, + }); + }, [accessToken]); + + return
{/* Chat UI */}
; +} +``` + +```tsx +// src/App.tsx +import { useState } from "react"; +import { ChatKitWidget } from "./ChatKitWidget"; + +function App() { + const [token] = useState("FAKE_TOKEN_FOR_DEV_ONLY"); + return ( +
+

React + ChatKit

+ +
+ ); +} + +export default App; +``` + +These examples together cover a full range of **frontend ChatKit patterns** +for teaching, debugging, and production integration. diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/reference.md b/.claude/skills/openai-chatkit-frontend-embed-skill/reference.md new file mode 100644 index 0000000..92008bd --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/reference.md @@ -0,0 +1,356 @@ +# OpenAI ChatKit – Frontend Embed Reference + +This reference document supports the `openai-chatkit-frontend-embed` Skill. +It standardizes **how you embed and configure ChatKit UI in a web frontend** +(Next.js / React / TS) for both **hosted workflows** and **custom backend** +setups. + +The goal: give students and developers a **single, opinionated pattern** for +wiring ChatKit into their apps in a secure and maintainable way. + +--- + +## 1. Scope of This Reference + +This file focuses on the **frontend layer only**: + +- How to install and import ChatKit JS/React packages. +- How to configure ChatKit for: + - Hosted workflows (Agent Builder). + - Custom backend (`api.url`, `fetch`, `uploadStrategy`, `domainKey`). +- How to pass auth and metadata from frontend → backend. +- How to debug common UI problems. + +Anything related to **ChatKit backend behavior** (Python, Agents SDK, tools, +business logic, etc.) belongs in the backend Skill/reference. + +--- + +## 2. Typical Frontend Stack Assumptions + +This reference assumes a modern TypeScript stack, for example: + +- **Next.js (App Router)** or +- **React (Vite/CRA)** + +with: + +- `NODE_ENV`-style environment variables (e.g. `NEXT_PUBLIC_*`). +- A separate **backend** domain or route (e.g. `https://api.example.com` + or `/api/chatkit` proxied to a backend). + +We treat ChatKit’s official package(s) as the source of truth for: + +- Import paths, +- Hooks/components, +- Config shapes. + +When ChatKit’s official API changes, update this reference accordingly. + +--- + +## 3. Installation & Basic Imports + +You will usually install a ChatKit package from npm, for example: + +```bash +npm install @openai/chatkit +# or a React-specific package such as: +npm install @openai/chatkit-react +``` + +> Note: Package names can evolve. Always confirm the exact name in the +> official ChatKit docs for your version. + +Basic patterns: + +```ts +// Example: using a ChatKit client factory or React provider +import { createChatKitClient } from "@openai/chatkit"; // example name +// or +import { ChatKitProvider, ChatKitWidget } from "@openai/chatkit-react"; +``` + +This Skill and reference do **not** invent APIs; they adapt to whichever +client/React API the docs specify for the version you are using. + +--- + +## 4. Two Main Modes: Hosted vs Custom Backend + +### 4.1 Hosted Workflow Mode (Agent Builder) + +In this mode: + +- ChatKit UI talks directly to OpenAI’s backend. +- Your frontend needs: + - A **workflow ID** (from Agent Builder, like `wf_...`). + - A **client token** or client secret that your backend mints. +- The backend endpoint (e.g. `/api/chatkit/token`) usually: + - Authenticates the user, + - Calls OpenAI to create a short-lived token, + - Sends that token back to the frontend. + +Frontend config shape (conceptual): + +```ts +const client = createChatKitClient({ + workflowId: process.env.NEXT_PUBLIC_CHATKIT_WORKFLOW_ID!, + async getClientToken() { + const res = await fetch("/api/chatkit/token", { credentials: "include" }); + if (!res.ok) throw new Error("Failed to fetch ChatKit token"); + const { clientSecret } = await res.json(); + return clientSecret; + }, + // domainKey, theme, etc. +}); +``` + +The logic of the conversation (tools, multi-agent flows, etc.) lives +primarily in **Agent Builder**, not in your code. + +### 4.2 Custom Backend Mode (Your Own Server) + +In this mode: + +- ChatKit UI talks to **your backend** instead of OpenAI directly. +- Frontend config uses a custom `api.url` and usually a custom `fetch`. + +High-level shape: + +```ts +const client = createChatKitClient({ + api: { + url: "https://api.example.com/chatkit/api", + fetch: async (url, options) => { + const accessToken = await getAccessTokenSomehow(); + return fetch(url, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${accessToken}`, + }, + credentials: "include", + }); + }, + uploadStrategy: { + type: "direct", + uploadUrl: "https://api.example.com/chatkit/api/upload", + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY, + }, + // other ChatKit options... +}); +``` + +In this setup: + +- Your **backend** validates auth and talks to the Agents SDK. +- ChatKit UI stays “dumb” about models/tools and just displays messages. + +**This reference prefers custom backend mode** for advanced use cases, +especially when using the Agents SDK with OpenAI/Gemini. + +--- + +## 5. Core Config Concepts + +Regardless of the exact ChatKit API, several config concepts recur. + +### 5.1 api.url + +- URL where the frontend sends ChatKit events. +- In custom backend mode it should point to your backend route, e.g.: + - `https://api.example.com/chatkit/api` (public backend), + - `/api/chatkit` (Next.js API route that proxies to backend). + +You should **avoid** hardcoding environment-dependent URLs inline; instead, +use environment variables: + +```ts +const CHATKIT_API_URL = + process.env.NEXT_PUBLIC_CHATKIT_API_URL ?? "/api/chatkit"; +``` + +### 5.2 api.fetch (Custom Fetch) + +Custom fetch allows you to inject auth and metadata: + +```ts +fetch: async (url, options) => { + const token = await getAccessToken(); + return fetch(url, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${token}`, + "X-User-Id": user.id, + "X-Tenant-Id": tenantId, + }, + credentials: "include", + }); +} +``` + +Key rules: + +- **Never** send raw OpenAI/Gemini API keys from the frontend. +- Only send short-lived access tokens or session cookies. +- If multi-tenant, send tenant identifiers as headers, not in query strings. + +### 5.3 uploadStrategy + +Controls how file uploads are handled. In custom backend mode you typically +use **direct upload** to your backend: + +```ts +uploadStrategy: { + type: "direct", + uploadUrl: CHATKIT_UPLOAD_URL, // e.g. "/api/chatkit/upload" +} +``` + +Backend responsibilities: + +- Accept `multipart/form-data`, +- Store files (disk, S3, etc.), +- Return a JSON body with a public URL and metadata expected by ChatKit. + +### 5.4 domainKey & Allowlisted Domains + +- ChatKit often requires a **domain allowlist** to decide which origins + are allowed to render the widget. +- A `domainKey` (or similar) is usually provided by OpenAI UI / dashboard. + +On the frontend: + +- Store it in `NEXT_PUBLIC_CHATKIT_DOMAIN_KEY` (or similar). +- Pass it through ChatKit config: + + ```ts + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY, + ``` + +If the widget is blank or disappears, check: + +- Is the origin (e.g. `https://app.example.com`) allowlisted? +- Is the `domainKey` correct and present? + +--- + +## 6. Recommended Next.js Organization + +For Next.js App Router (TypeScript), a common structure: + +```text +src/ + app/ + chat/ + page.tsx # Chat page using ChatKit + components/ + chatkit/ + ChatKitProvider.tsx + ChatKitWidget.tsx + chatkitClient.ts # optional client factory +``` + +### 6.1 ChatKitProvider.tsx (Conceptual) + +- Wraps your chat tree with the ChatKit context/provider. +- Injects ChatKit client config in one place. + +### 6.2 ChatKitWidget.tsx + +- A focused component that renders the actual Chat UI. +- Receives props like `user`, `tenantId`, optional initial messages. + +### 6.3 Environment Variables + +Use `NEXT_PUBLIC_...` only for **non-secret** values: + +- `NEXT_PUBLIC_CHATKIT_DOMAIN_KEY` +- `NEXT_PUBLIC_CHATKIT_API_URL` +- `NEXT_PUBLIC_CHATKIT_WORKFLOW_ID` (if using hosted workflows) + +Secrets belong on the backend side. + +--- + +## 7. Debugging & Common Issues + +### 7.1 Widget Not Showing / Blank + +Checklist: + +1. Check browser console for errors. +2. Confirm correct import paths / package versions. +3. Verify **domain allowlist** and `domainKey` configuration. +4. Check network tab: + - Are `chatkit` requests being sent? + - Any 4xx/5xx or CORS errors? +5. If using custom backend: + - Confirm the backend route exists and returns a valid response shape. + +### 7.2 “Loading…” Never Finishes + +- Usually indicates backend is not returning expected structure or stream. +- Add logging to backend for incoming ChatKit events and outgoing responses. +- Temporarily log responses on the frontend to inspect their shape. + +### 7.3 File Uploads Fail + +- Ensure `uploadUrl` points to a backend route that accepts `multipart/form-data`. +- Check response body shape matches ChatKit’s expectation (URL field, etc.). +- Inspect network tab to confirm request/response. + +### 7.4 Auth / 401 Errors + +- Confirm that your custom `fetch` attaches the correct token or cookie. +- Confirm backend checks that token and does not fail with generic 401/403. +- In dev, log incoming headers on backend for debugging (but never log + secrets to console in production). + +--- + +## 8. Evolving with ChatKit Versions + +ChatKit’s API may change over time (prop names, hooks, config keys). To keep +this Skill and your code up to date: + +- Treat **official ChatKit docs** as the top source of truth for frontend + API details. +- If you have ChatKit docs via MCP (e.g. `chatkit/frontend/latest.md`, + `chatkit/changelog.md`), prefer them over older examples. +- When you detect a mismatch (e.g. a prop is renamed or removed): + - Update your local templates/components. + - Update this reference file. + +A good practice is to maintain a short local changelog next to this file +documenting which ChatKit version the examples were last validated against. + +--- + +## 9. Teaching & Best Practices Summary + +When using this Skill and reference to teach students or onboard teammates: + +- Start with a **simple, working embed**: + - Hosted workflow mode OR + - Custom backend that just echoes messages. +- Then layer in: + - Auth header injection, + - File uploads, + - Multi-tenant headers, + - Theming and layout. + +Enforce these best practices: + +- No API keys in frontend code. +- Single, centralized ChatKit config (not scattered across components). +- Clear separation of concerns: + - Frontend: UI + ChatKit config. + - Backend: Auth + business logic + Agents SDK. + +By following this reference, the `openai-chatkit-frontend-embed` Skill can +generate **consistent, secure, and maintainable** ChatKit frontend code +across projects. diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitProvider.tsx b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitProvider.tsx new file mode 100644 index 0000000..894eb50 --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitProvider.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React, { createContext, useContext, useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type ChatKitContextValue = { + client: any; +}; + +const ChatKitContext = createContext(null); + +type Props = { + accessToken: string; + children: React.ReactNode; +}; + +export function ChatKitProvider({ accessToken, children }: Props) { + const value = useMemo(() => { + const client = createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + return fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + return { client }; + }, [accessToken]); + + return ( + + {children} + + ); +} + +export function useChatKit() { + const ctx = useContext(ChatKitContext); + if (!ctx) throw new Error("useChatKit must be used in provider"); + return ctx; +} diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitWidget.tsx b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitWidget.tsx new file mode 100644 index 0000000..d83986c --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitWidget.tsx @@ -0,0 +1,16 @@ +"use client"; + +import React from "react"; +import { useChatKit } from "./ChatKitProvider"; + +export function ChatKitWidget() { + const { client } = useChatKit(); + + return ( +
+

+ ChatKit UI will render here with client instance. +

+
+ ); +} diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/templates/FloatingChatButton.tsx b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/FloatingChatButton.tsx new file mode 100644 index 0000000..bae4000 --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/FloatingChatButton.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useState } from "react"; +import { ChatKitWidget } from "./ChatKitWidget"; + +export function FloatingChatButton({ accessToken }: { accessToken: string }) { + const [open, setOpen] = useState(false); + + return ( + <> + {open && ( +
+ +
+ )} + + + + ); +} diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/templates/makeFetch.ts b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/makeFetch.ts new file mode 100644 index 0000000..882dc78 --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/makeFetch.ts @@ -0,0 +1,11 @@ +export function makeChatKitFetch(accessToken: string, extras?: Record) { + return async (url: string, options: RequestInit) => { + const headers: HeadersInit = { + ...(options.headers || {}), + Authorization: `Bearer ${accessToken}`, + ...(extras || {}), + }; + + return fetch(url, { ...options, headers }); + }; +} diff --git a/.claude/skills/python-cli-todo-skill/SKILL.md b/.claude/skills/python-cli-todo-skill/SKILL.md deleted file mode 100644 index a84b3df..0000000 --- a/.claude/skills/python-cli-todo-skill/SKILL.md +++ /dev/null @@ -1,49 +0,0 @@ -name: python-cli-todo-skill -version: 0.1.0 -description: This skill is designed to build, maintain, test, and debug an in-memory Python todo console application. It should be invoked whenever the user explicitly requests to work on the Python todo application, whether for new feature development, bug fixes, or testing purposes. -allowed-tools: Write, Edit, Read, Grep, Glob, Bash - ---- -# Python CLI Todo Skill (v0.1.0) - -This skill provides specialized capabilities for developing and maintaining an in-memory Python todo console application. - -## When to Use This Skill: - -Invoke this skill when the user's request clearly pertains to: -* Developing new features for the Python todo application. -* Debugging existing issues within the todo app. -* Writing or running tests for the todo application. -* Refactoring or improving the code quality of the todo app. -* Any task directly related to the "in-memory Python todo console application". - -## How to Use This Skill: - -Once invoked, the following guidelines should be followed: - -1. **Understand the Request**: Carefully read the user's prompt to determine the specific task (e.g., "add a new todo," "mark a todo as complete," "fix a bug in listing todos"). - -2. **Explore the Codebase (if necessary)**: Use `Read`, `Glob`, and `Grep` tools to understand the existing structure, functions, and logic of the Python todo application. - * **Example**: To find the main application file, you might use `Glob(pattern='**/*main.py')` or `Grep(pattern='def main', type='py')`. - -3. **Plan the Implementation**: For complex tasks, use the `TodoWrite` tool to break down the task into smaller, manageable steps. - -4. **Implement or Modify Code**: Use the `Write` or `Edit` tools to make necessary code changes. - * **Example**: `Edit(file_path='todo_app.py', old_string='def add_item(', new_string='def add_todo_item(')` - -5. **Test Changes**: Use the `Bash` tool to run tests or directly execute the Python script to verify changes. - * **Example**: `Bash(command='pytest tests/test_todo.py', description='Run unit tests for todo application')` - * **Example**: `Bash(command='python todo_app.py', description='Run the todo application')` - -6. **Debug (if needed)**: If tests fail or unexpected behavior occurs, use `Read`, `Grep`, and `Bash` (for running with print statements or debuggers) to identify and fix issues. - -7. **Inform the User**: Provide concise updates on progress and outcomes. - -## Allowed Tools: - -* `Write`: To create new files or overwrite existing ones. -* `Edit`: To modify specific parts of a file. -* `Read`: To view the content of files. -* `Grep`: To search for patterns within files. -* `Glob`: To find files by pattern. -* `Bash`: For executing shell commands (e.g., running Python scripts, tests, `ls`). diff --git a/.claude/skills/shadcn/SKILL.md b/.claude/skills/shadcn/SKILL.md new file mode 100644 index 0000000..2e8b3c7 --- /dev/null +++ b/.claude/skills/shadcn/SKILL.md @@ -0,0 +1,254 @@ +--- +name: shadcn +description: Comprehensive shadcn/ui component library with theming, customization patterns, and accessibility. Use when building modern React UIs with Tailwind CSS. IMPORTANT - Always use MCP server tools first when available. +--- + +# shadcn/ui Skill + +Beautiful, accessible components built with Radix UI and Tailwind CSS. Copy and paste into your apps. + +## MCP Server Integration (PRIORITY) + +**ALWAYS check and use MCP server tools first:** + +``` +# 1. Check availability +mcp__shadcn__get_project_registries + +# 2. Search components +mcp__shadcn__search_items_in_registries + registries: ["@shadcn"] + query: "button" + +# 3. Get examples +mcp__shadcn__get_item_examples_from_registries + registries: ["@shadcn"] + query: "button-demo" + +# 4. Get install command +mcp__shadcn__get_add_command_for_items + items: ["@shadcn/button"] + +# 5. Verify implementation +mcp__shadcn__get_audit_checklist +``` + +## Quick Start + +### Installation + +```bash +# Initialize shadcn in your project +npx shadcn@latest init + +# Add components +npx shadcn@latest add button +npx shadcn@latest add card +npx shadcn@latest add input +``` + +### Project Structure + +``` +src/ +├── components/ +│ └── ui/ # shadcn components +│ ├── button.tsx +│ ├── card.tsx +│ └── input.tsx +├── lib/ +│ └── utils.ts # cn() utility +└── app/ + └── globals.css # CSS variables +``` + +## Key Concepts + +| Concept | Guide | +|---------|-------| +| **Theming** | [reference/theming.md](reference/theming.md) | +| **Accessibility** | [reference/accessibility.md](reference/accessibility.md) | +| **Animations** | [reference/animations.md](reference/animations.md) | +| **Components** | [reference/components.md](reference/components.md) | + +## Examples + +| Pattern | Guide | +|---------|-------| +| **Form Patterns** | [examples/form-patterns.md](examples/form-patterns.md) | +| **Data Display** | [examples/data-display.md](examples/data-display.md) | +| **Navigation** | [examples/navigation.md](examples/navigation.md) | +| **Feedback** | [examples/feedback.md](examples/feedback.md) | + +## Templates + +| Template | Purpose | +|----------|---------| +| [templates/theme-config.ts](templates/theme-config.ts) | Tailwind theme extension | +| [templates/component-scaffold.tsx](templates/component-scaffold.tsx) | Base component with variants | +| [templates/form-template.tsx](templates/form-template.tsx) | Form with validation | + +## Component Categories + +### Inputs +- Button, Input, Textarea, Select, Checkbox, Radio, Switch, Slider + +### Data Display +- Card, Table, Avatar, Badge, Calendar + +### Feedback +- Alert, Toast, Dialog, Sheet, Tooltip, Popover + +### Navigation +- Tabs, Navigation Menu, Breadcrumb, Pagination + +### Layout +- Accordion, Collapsible, Separator, Scroll Area + +## Theming System + +### CSS Variables + +```css +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + /* ... */ + } +} +``` + +### Dark Mode Toggle + +```tsx +"use client"; + +import { useTheme } from "next-themes"; +import { Button } from "@/components/ui/button"; +import { Moon, Sun } from "lucide-react"; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +} +``` + +## Utility Function + +```typescript +// lib/utils.ts +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} +``` + +## Common Patterns + +### Form with Validation + +```tsx +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +const schema = z.object({ + email: z.string().email(), + password: z.string().min(8), +}); + +function LoginForm() { + const form = useForm({ + resolver: zodResolver(schema), + }); + + return ( +
+ + ( + + Email + + + + + + )} + /> + + + + ); +} +``` + +### Toast Notifications + +```tsx +import { toast } from "sonner"; + +// Success +toast.success("Task created successfully"); + +// Error +toast.error("Something went wrong"); + +// With action +toast("Event created", { + action: { + label: "Undo", + onClick: () => console.log("Undo"), + }, +}); +``` + +## Accessibility Checklist + +- [ ] All interactive elements are keyboard accessible +- [ ] Focus states are visible +- [ ] Color contrast meets WCAG AA (4.5:1 for text) +- [ ] ARIA labels on icon-only buttons +- [ ] Form inputs have associated labels +- [ ] Error messages are announced to screen readers +- [ ] Dialogs trap focus and return focus on close +- [ ] Reduced motion preferences respected diff --git a/.claude/skills/shadcn/examples/data-display.md b/.claude/skills/shadcn/examples/data-display.md new file mode 100644 index 0000000..8084077 --- /dev/null +++ b/.claude/skills/shadcn/examples/data-display.md @@ -0,0 +1,410 @@ +# Data Display Patterns + +Examples for displaying data with cards, tables, lists, and data grids. + +## Basic Card + +```tsx +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + +export function BasicCard() { + return ( + + + Card Title + Card description goes here. + + +

Card content and details.

+
+ + + + +
+ ); +} +``` + +## Task Card with Actions + +```tsx +interface Task { + id: number; + title: string; + description?: string; + completed: boolean; + createdAt: Date; +} + +export function TaskCard({ task, onToggle, onEdit, onDelete }: { + task: Task; + onToggle: () => void; + onEdit: () => void; + onDelete: () => void; +}) { + return ( + + +
+
+ + + {task.title} + +
+ + + + + + + + Edit + + + + Delete + + + +
+
+ {task.description && ( + +

{task.description}

+
+ )} + + Created {formatDate(task.createdAt)} + +
+ ); +} +``` + +## Stats Cards + +```tsx +interface Stat { + title: string; + value: string | number; + change?: number; + icon: React.ReactNode; +} + +export function StatsCard({ stat }: { stat: Stat }) { + return ( + + + + {stat.title} + + {stat.icon} + + +
{stat.value}
+ {stat.change !== undefined && ( +

= 0 ? "text-green-600" : "text-red-600" + )}> + {stat.change >= 0 ? "+" : ""}{stat.change}% from last month +

+ )} +
+
+ ); +} + +export function StatsGrid({ stats }: { stats: Stat[] }) { + return ( +
+ {stats.map((stat, index) => ( + + ))} +
+ ); +} +``` + +## Data Table + +```tsx +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +interface User { + id: number; + name: string; + email: string; + role: string; + status: "active" | "inactive"; +} + +export function UsersTable({ users }: { users: User[] }) { + return ( +
+ + + + Name + Email + Role + Status + Actions + + + + {users.length === 0 ? ( + + + No users found. + + + ) : ( + users.map((user) => ( + + {user.name} + {user.email} + + {user.role} + + + + {user.status} + + + + + + + + + View + Edit + + Delete + + + + + + )) + )} + +
+
+ ); +} +``` + +## Card Grid with Skeleton Loading + +```tsx +export function CardGrid({ items, isLoading }) { + if (isLoading) { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + + + + + + + + + + + + ))} +
+ ); + } + + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +} +``` + +## Empty State + +```tsx +export function EmptyState({ + icon: Icon, + title, + description, + action, +}: { + icon: React.ComponentType<{ className?: string }>; + title: string; + description: string; + action?: React.ReactNode; +}) { + return ( +
+
+ +
+

{title}

+

+ {description} +

+ {action &&
{action}
} +
+ ); +} + +// Usage + + + Add Task + + } +/> +``` + +## List with Avatar + +```tsx +export function UserList({ users }) { + return ( +
+ {users.map((user) => ( +
+
+ + + {user.name.slice(0, 2).toUpperCase()} + +
+

{user.name}

+

{user.email}

+
+
+ +
+ ))} +
+ ); +} +``` + +## Pagination + +```tsx +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; + +export function DataPagination({ + currentPage, + totalPages, + onPageChange, +}: { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +}) { + return ( + + + + onPageChange(currentPage - 1)} + aria-disabled={currentPage === 1} + /> + + {/* Page numbers */} + {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter((page) => { + return ( + page === 1 || + page === totalPages || + Math.abs(page - currentPage) <= 1 + ); + }) + .map((page, index, array) => ( + + {index > 0 && array[index - 1] !== page - 1 && ( + + + + )} + + onPageChange(page)} + isActive={currentPage === page} + > + {page} + + + + ))} + + onPageChange(currentPage + 1)} + aria-disabled={currentPage === totalPages} + /> + + + + ); +} +``` diff --git a/.claude/skills/shadcn/examples/feedback.md b/.claude/skills/shadcn/examples/feedback.md new file mode 100644 index 0000000..1afa40f --- /dev/null +++ b/.claude/skills/shadcn/examples/feedback.md @@ -0,0 +1,408 @@ +# Feedback Patterns + +Examples for alerts, toasts, dialogs, and loading states. + +## Alert Messages + +```tsx +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { AlertCircle, CheckCircle2, Info, AlertTriangle } from "lucide-react"; + +// Success Alert + + + Success + + Your changes have been saved successfully. + + + +// Error Alert + + + Error + + Something went wrong. Please try again later. + + + +// Warning Alert + + + Warning + + Your session will expire in 5 minutes. + + + +// Info Alert + + + Note + + This feature is currently in beta. + + +``` + +## Toast Notifications (Sonner) + +```tsx +// Setup: Add Toaster to layout +import { Toaster } from "@/components/ui/sonner"; + +// app/layout.tsx +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ); +} + +// Usage +import { toast } from "sonner"; + +// Basic toasts +toast("Event created"); +toast.success("Successfully saved!"); +toast.error("Something went wrong"); +toast.warning("Please review your input"); +toast.info("New update available"); + +// With description +toast.success("Task completed", { + description: "Your task has been marked as done.", +}); + +// With action +toast("File uploaded", { + action: { + label: "View", + onClick: () => router.push("/files"), + }, +}); + +// With cancel +toast("Delete item?", { + action: { + label: "Delete", + onClick: () => deleteItem(), + }, + cancel: { + label: "Cancel", + onClick: () => {}, + }, +}); + +// Promise toast (loading → success/error) +toast.promise(saveData(), { + loading: "Saving...", + success: "Data saved successfully!", + error: "Failed to save data", +}); + +// Custom duration +toast.success("Saved!", { duration: 5000 }); // 5 seconds + +// Dismiss programmatically +const toastId = toast.loading("Loading..."); +// Later: +toast.dismiss(toastId); +``` + +## Confirmation Dialog + +```tsx +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; + +export function DeleteConfirmation({ onConfirm, itemName }) { + return ( + + + + + + + Are you sure? + + This will permanently delete "{itemName}". This action cannot be undone. + + + + Cancel + + Delete + + + + + ); +} +``` + +## Form Dialog + +```tsx +export function CreateTaskDialog({ onSubmit }) { + const [open, setOpen] = useState(false); + + function handleSubmit(data: FormData) { + onSubmit(data); + setOpen(false); + } + + return ( + + + + + + + Create Task + + Add a new task to your list. + + +
+
+
+ + +
+
+ +