brainstorming
Structured design dialogue that validates ideas before implementation begins.
openai-chatkit-skill
Integrate OpenAI ChatKit for conversational AI interface
Full skill instructions
This skill provides guidance for integrating OpenAI's ChatKit interface for conversational AI experiences.
src/
├── components/
│ └── chat/
│ ├── ChatWindow.tsx # Main chat container
│ ├── ChatMessage.tsx # Individual message display
│ ├── ChatInput.tsx # User input area
│ ├── ChatToolbar.tsx # Quick actions toolbar
│ └── typing-indicator.tsx
├── hooks/
│ └── useChat.ts # Chat state management
├── lib/
│ └── openai.ts # OpenAI client configuration
└── types/
└── chat.ts # Chat type definitions
// src/types/chat.ts
export interface ChatMessage {
id: string
role: 'user' | 'assistant' | 'system'
content: string
timestamp: Date
tools?: ToolCall[]
}
export interface ToolCall {
id: string
name: string
arguments: Record<string, unknown>
result?: unknown
}
export interface ChatState {
messages: ChatMessage[]
isLoading: boolean
error: string | null
}
export interface UseChatReturn extends ChatState {
sendMessage: (content: string) => Promise<void>
clearMessages: () => void
retry: () => Promise<void>
}
// src/lib/openai.ts
import OpenAI from 'openai'
export const openai = new OpenAI({
apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY || process.env.OPENAI_API_KEY,
})
export interface ChatCompletionOptions {
model?: string
temperature?: number
max_tokens?: number
stream?: boolean
}
export async function createChatCompletion(
messages: ChatMessage[],
options: ChatCompletionOptions = {}
) {
const response = await openai.chat.completions.create({
model: options.model || 'gpt-4o',
messages: messages.map(m => ({
role: m.role,
content: m.content,
})),
temperature: options.temperature ?? 0.7,
max_tokens: options.max_tokens ?? 1024,
stream: options.stream ?? false,
tools: getToolDefinitions(),
})
return response
}
// Tool definitions for the AI
export function getToolDefinitions() {
return [
{
type: 'function',
function: {
name: 'get_tasks',
description: 'Get all tasks for the current user',
parameters: {
type: 'object',
properties: {},
},
},
},
{
type: 'function',
function: {
name: 'create_task',
description: 'Create a new task',
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: 'Task title' },
description: { type: 'string', description: 'Optional description' },
due_date: { type: 'string', description: 'Optional due date' },
},
required: ['title'],
},
},
},
{
type: 'function',
function: {
name: 'update_task',
description: 'Update an existing task',
parameters: {
type: 'object',
properties: {
task_id: { type: 'number', description: 'Task ID' },
title: { type: 'string', description: 'New title' },
completed: { type: 'boolean', description: 'Completion status' },
},
required: ['task_id'],
},
},
},
{
type: 'function',
function: {
name: 'delete_task',
description: 'Delete a task',
parameters: {
type: 'object',
properties: {
task_id: { type: 'number', description: 'Task ID to delete' },
},
required: ['task_id'],
},
},
},
]
}
// src/hooks/useChat.ts
'use client'
import { useCallback, useState } from 'react'
import { ChatMessage, UseChatReturn } from '@/types/chat'
import { createChatCompletion } from '@/lib/openai'
import { v4 as uuidv4 } from 'uuid'
const SYSTEM_PROMPT = `You are a helpful AI assistant for task management.
You can help users create, update, and manage their tasks through natural language.
Always be helpful, friendly, and concise in your responses.`
export function useChat(): UseChatReturn {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: 'system',
role: 'system',
content: SYSTEM_PROMPT,
timestamp: new Date(),
},
])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const sendMessage = useCallback(async (content: string) => {
const userMessage: ChatMessage = {
id: uuidv4(),
role: 'user',
content,
timestamp: new Date(),
}
setMessages(prev => [...prev, userMessage])
setIsLoading(true)
setError(null)
try {
const response = await createChatCompletion(
[...messages, userMessage],
{ stream: false }
)
const assistantMessage: ChatMessage = {
id: uuidv4(),
role: 'assistant',
content: response.choices[0]?.message?.content || 'No response',
timestamp: new Date(),
}
setMessages(prev => [...prev, assistantMessage])
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setIsLoading(false)
}
}, [messages])
const clearMessages = useCallback(() => {
setMessages([
{
id: 'system',
role: 'system',
content: SYSTEM_PROMPT,
timestamp: new Date(),
},
])
}, [])
const retry = useCallback(async () => {
const lastUserMessage = [...messages].reverse().find(m => m.role === 'user')
if (lastUserMessage) {
await sendMessage(lastUserMessage.content)
}
}, [messages, sendMessage])
return {
messages,
isLoading,
error,
sendMessage,
clearMessages,
retry,
}
}
// src/components/chat/ChatWindow.tsx
'use client'
import { useRef, useEffect } from 'react'
import { useChat } from '@/hooks/useChat'
import { ChatMessage } from './ChatMessage'
import { ChatInput } from './ChatInput'
export function ChatWindow() {
const { messages, isLoading, error, sendMessage } = useChat()
const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
const handleSend = async (content: string) => {
await sendMessage(content)
}
const assistantMessages = messages.filter(m => m.role === 'assistant')
const userMessages = messages.filter(m => m.role === 'user')
return (
<div className="flex flex-col h-[600px] max-w-2xl mx-auto border rounded-xl overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-4">
<h2 className="text-lg font-semibold">AI Assistant</h2>
<p className="text-sm opacity-80">Ask me to help with your tasks</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
{messages
.filter(m => m.role !== 'system')
.map((message, index) => (
<ChatMessage
key={message.id}
message={message}
isLatest={index === messages.length - 2}
/>
))}
{isLoading && (
<div className="flex items-center gap-2 text-gray-500">
<div className="flex gap-1">
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" />
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" />
</div>
<span className="text-sm">AI is thinking...</span>
</div>
)}
{error && (
<div className="p-3 bg-red-50 text-red-700 rounded-lg">
Error: {error}
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<ChatInput onSend={handleSend} isLoading={isLoading} />
</div>
)
}
// src/components/chat/ChatMessage.tsx
import { ChatMessage as ChatMessageType } from '@/types/chat'
interface ChatMessageProps {
message: ChatMessageType
isLatest?: boolean
}
export function ChatMessage({ message, isLatest }: ChatMessageProps) {
const isUser = message.role === 'user'
return (
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[80%] ${isUser ? 'order-2' : 'order-1'}`}>
{/* Avatar */}
<div className={`flex items-center gap-2 mb-1 ${isUser ? 'flex-row-reverse' : ''}`}>
<div className={`
w-8 h-8 rounded-full flex items-center justify-center text-white text-sm
${isUser ? 'bg-blue-500' : 'bg-gradient-to-br from-purple-500 to-pink-500'}
`}>
{isUser ? 'U' : 'AI'}
</div>
<span className="text-xs text-gray-500">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
{/* Message bubble */}
<div className={`
rounded-2xl px-4 py-3
${isUser
? 'bg-blue-500 text-white rounded-tr-sm'
: 'bg-white border shadow-sm rounded-tl-sm'
}
`}>
<p className="whitespace-pre-wrap leading-relaxed">
{message.content}
</p>
</div>
{/* Tool calls indicator */}
{message.tools && message.tools.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{message.tools.map(tool => (
<span
key={tool.id}
className="inline-flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs"
>
<span className="w-2 h-2 bg-purple-500 rounded-full" />
{tool.name}
</span>
))}
</div>
)}
</div>
</div>
)
}
// src/components/chat/ChatInput.tsx
'use client'
import { useState, useRef, useEffect } from 'react'
interface ChatInputProps {
onSend: (content: string) => Promise<void>
isLoading: boolean
}
export function ChatInput({ onSend, isLoading }: ChatInputProps) {
const [input, setInput] = useState('')
const textareaRef = useRef<HTMLTextAreaElement>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!input.trim() || isLoading) return
const content = input.trim()
setInput('')
await onSend(content)
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit(e)
}
}
// Auto-resize textarea
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto'
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`
}
}, [input])
return (
<form onSubmit={handleSubmit} className="p-4 bg-white border-t">
<div className="flex items-end gap-2">
<textarea
ref={textareaRef}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask me to help with your tasks..."
className="flex-1 resize-none border rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent max-h-32"
rows={1}
disabled={isLoading}
/>
<button
type="submit"
disabled={!input.trim() || isLoading}
className={`
px-4 py-2 rounded-lg font-medium transition-colors
${input.trim() && !isLoading
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}
`}
>
{isLoading ? 'Sending...' : 'Send'}
</button>
</div>
<p className="text-xs text-gray-500 mt-2">
Press Enter to send, Shift+Enter for new line
</p>
</form>
)
}
// Streaming chat implementation
export async function* streamChat(messages: ChatMessage[]) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: messages.map(m => ({ role: m.role, content: m.content })),
stream: true,
})
for await (const chunk of response) {
const content = chunk.choices[0]?.delta?.content
if (content) {
yield content
}
}
}
// Usage in component
async function handleStreamingSend(content: string) {
const encoder = new TextEncoder()
const stream = await streamChat([...messages, userMessage])
// Display streaming content
for await (const chunk of stream) {
setAssistantContent(prev => prev + chunk)
}
}
Structured design dialogue that validates ideas before implementation begins.
Comprehensive design intelligence for web and mobile UI/UX across 10 technology stacks.
Comprehensive implementation plans for multi-step tasks, breaking down specs into bite-sized, testable steps.
Introduction to the obra skills system with mandatory skill invocation rules and best practices.
Execute a written implementation plan with critical review and task checkpoints.
Delegate independent tasks to specialized agents working concurrently with isolated context.
Isolated git worktrees with smart directory selection and safety verification.
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Plan searchable and shareable content that drives traffic, builds authority, and generates leads.
README-first repository scanner that extracts commands and classifies reproduction candidates without executing them.
Brainstorm and prioritize marketing strategies tailored to your SaaS stage, budget, and goals.
Plan and optimize your website's page hierarchy, navigation, URL structure, and internal linking.
The world's fastest calendar for remote work
Capture, organize, and utilize your knowledge effortlessly.
Instant Summaries of Audio & Video Interviews with AnySummary
Transform PDFs into engaging mind maps.
Chat with any PDF instantly
Create an optimal daily plan using your voice
A productivity platform to centralize organizational knowledge and workflows with contextual AI assistance.
AskYourPDF Pricing Plans: Tailored to Your Needs