
Building Ai Chat
- 112 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
Building-ai-chat is a Claude Code skill that builds AI chat and conversational UI with streaming responses, context management, and multi-modal support.
About
Building-ai-chat is a Claude Code skill that provides patterns and React/TSX components for AI chat and conversational interfaces. A developer uses it when creating ChatGPT-style UIs, assistants, or copilots that need streaming text, context and token management, and multi-modal input. It covers response controls (stop, regenerate, edit), feedback mechanisms, auto-scroll heuristics, and AI-specific error handling like refusals.
- Streaming message components with Streamdown for incomplete markdown
- Token-limit indicators, regeneration, and feedback controls
- Multi-modal image, file, and voice input patterns
Building Ai Chat by the numbers
- 112 all-time installs (skills.sh)
- Ranked #1,029 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
building-ai-chat capabilities & compatibility
- Capabilities
- building forms · building tables · creating dashboards · building ai chat
- Works with
- vercel
- Use cases
- frontend · ui design
What building-ai-chat says it does
Builds AI chat interfaces and conversational UI with streaming responses, context management, and multi-modal support.
Use Streamdown for AI streaming (handles incomplete markdown)
npx skills add https://github.com/ancoleman/ai-design-components --skill building-ai-chatAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 112 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Build a streaming AI chat interface with message bubbles, token indicators, regeneration, and multi-modal input.
Who is it for?
Building ChatGPT-style conversational interfaces with streaming text and multi-modal inputs.
Skip if: Backend model inference or LLM API orchestration.
When should I use this skill?
Building an AI assistant, copilot, or chatbot UI with streaming responses.
What you get
Battle-tested chat components with streaming, feedback loops, and AI-specific error handling.
- Message display components
- Input components with attachments and voice
- Streaming and auto-scroll patterns
By the numbers
- Minimal AI chat interface in under 50 lines
Files
AI Chat Interface Components
Purpose
Define the emerging standards for AI/human conversational interfaces in the 2024-2025 AI integration boom. This skill leverages meta-knowledge from building WITH Claude to establish definitive patterns for streaming UX, context management, and multi-modal interactions. As the industry lacks established patterns, this provides the reference implementation others will follow.
When to Use
Activate this skill when:
- Building ChatGPT-style conversational interfaces
- Creating AI assistants, copilots, or chatbots
- Implementing streaming text responses with markdown
- Managing conversation context and token limits
- Handling multi-modal inputs (text, images, files, voice)
- Dealing with AI-specific errors (hallucinations, refusals, limits)
- Adding feedback mechanisms (thumbs, regeneration, editing)
- Implementing conversation branching or threading
- Visualizing tool/function calling
Quick Start
Minimal AI chat interface in under 50 lines:
import { useChat } from 'ai/react';
export function MinimalAIChat() {
const { messages, input, handleInputChange, handleSubmit, isLoading, stop } = useChat();
return (
<div className="chat-container">
<div className="messages">
{messages.map(m => (
<div key={m.id} className={`message ${m.role}`}>
<div className="content">{m.content}</div>
</div>
))}
{isLoading && <div className="thinking">AI is thinking...</div>}
</div>
<form onSubmit={handleSubmit} className="input-form">
<input
value={input}
onChange={handleInputChange}
placeholder="Ask anything..."
disabled={isLoading}
/>
{isLoading ? (
<button type="button" onClick={stop}>Stop</button>
) : (
<button type="submit">Send</button>
)}
</form>
</div>
);
}For complete implementation with streaming markdown, see examples/basic-chat.tsx.
Core Components
Message Display
Build user, AI, and system message bubbles with streaming support:
// User message
<div className="message user">
<div className="content">{message.content}</div>
<time className="timestamp">{formatTime(message.timestamp)}</time>
</div>
// AI message with streaming
<div className="message ai">
<Streamdown className="content">{message.content}</Streamdown>
{message.isStreaming && <span className="cursor">▊</span>}
</div>
// System message
<div className="message system">
<Icon type="info" />
<span>{message.content}</span>
</div>For markdown rendering, code blocks, and formatting details, see references/message-components.md.
Input Components
Create rich input experiences with attachments and voice:
<div className="input-container">
<button onClick={attachFile} aria-label="Attach file">
<PaperclipIcon />
</button>
<textarea
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
rows={1}
style={{ height: textareaHeight }}
/>
<button onClick={toggleVoice} aria-label="Voice input">
<MicIcon />
</button>
<button type="submit" disabled={!input.trim() || isLoading}>
<SendIcon />
</button>
</div>Response Controls
Essential controls for AI responses:
<div className="response-controls">
{isStreaming && (
<button onClick={stop} className="stop-btn">
Stop generating
</button>
)}
{!isStreaming && (
<>
<button onClick={regenerate} aria-label="Regenerate response">
<RefreshIcon /> Regenerate
</button>
<button onClick={continueGeneration} aria-label="Continue">
Continue
</button>
<button onClick={editMessage} aria-label="Edit message">
<EditIcon /> Edit
</button>
</>
)}
</div>Feedback Mechanisms
Collect user feedback to improve AI responses:
<div className="feedback-controls">
<button
onClick={() => sendFeedback('positive')}
aria-label="Good response"
className={feedback === 'positive' ? 'selected' : ''}
>
<ThumbsUpIcon />
</button>
<button
onClick={() => sendFeedback('negative')}
aria-label="Bad response"
className={feedback === 'negative' ? 'selected' : ''}
>
<ThumbsDownIcon />
</button>
<button onClick={copyToClipboard} aria-label="Copy">
<CopyIcon />
</button>
<button onClick={share} aria-label="Share">
<ShareIcon />
</button>
</div>Streaming & Real-Time UX
Progressive rendering of AI responses requires special handling:
// Use Streamdown for AI streaming (handles incomplete markdown)
import { Streamdown } from '@vercel/streamdown';
// Auto-scroll management
useEffect(() => {
if (shouldAutoScroll()) {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [messages]);
// Smart auto-scroll heuristic
function shouldAutoScroll() {
const threshold = 100; // px from bottom
const isNearBottom =
container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
const userNotReading = !hasUserScrolledUp && !isTextSelected;
return isNearBottom && userNotReading;
}For complete streaming patterns, auto-scroll behavior, and stop generation, see references/streaming-ux.md.
Context Management
Communicate token limits clearly to users:
// User-friendly token display
function TokenIndicator({ used, total }) {
const percentage = (used / total) * 100;
const remaining = total - used;
return (
<div className="token-indicator">
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${percentage}%` }} />
</div>
<span className="token-text">
{percentage > 80
? `⚠️ About ${Math.floor(remaining / 250)} messages left`
: `${Math.floor(remaining / 250)} pages of conversation remaining`}
</span>
</div>
);
}For summarization strategies, conversation branching, and organization, see references/context-management.md.
Multi-Modal Support
Handle images, files, and voice inputs:
// Image upload with preview
function ImageUpload({ onUpload }) {
return (
<div
className="upload-zone"
onDrop={handleDrop}
onDragOver={preventDefault}
>
<input
type="file"
accept="image/*"
onChange={handleFileSelect}
multiple
hidden
ref={fileInputRef}
/>
{previews.map(preview => (
<img key={preview.id} src={preview.url} alt="Upload preview" />
))}
</div>
);
}For complete multi-modal patterns including voice and screen sharing, see references/multi-modal.md.
Error Handling
Handle AI-specific errors gracefully:
// Refusal handling
if (response.type === 'refusal') {
return (
<div className="error refusal">
<Icon type="info" />
<p>I cannot help with that request.</p>
<details>
<summary>Why?</summary>
<p>{response.reason}</p>
</details>
<p>Try asking: {response.suggestion}</p>
</div>
);
}
// Rate limit communication
if (error.code === 'RATE_LIMIT') {
return (
<div className="error rate-limit">
<p>Please wait {error.retryAfter} seconds</p>
<CountdownTimer seconds={error.retryAfter} onComplete={retry} />
</div>
);
}For comprehensive error patterns, see references/error-handling.md.
Tool Usage Visualization
Show when AI is using tools or functions:
function ToolUsage({ tool }) {
return (
<div className="tool-usage">
<div className="tool-header">
<Icon type={tool.type} />
<span>{tool.name}</span>
{tool.status === 'running' && <Spinner />}
</div>
{tool.status === 'complete' && (
<details>
<summary>View details</summary>
<pre>{JSON.stringify(tool.result, null, 2)}</pre>
</details>
)}
</div>
);
}For function calling, code execution, and web search patterns, see references/tool-usage.md.
Implementation Guide
Recommended Stack
Primary libraries (validated November 2025):
# Core AI chat functionality
npm install ai @ai-sdk/react @ai-sdk/openai
# Streaming markdown rendering
npm install @vercel/streamdown
# Syntax highlighting
npm install react-syntax-highlighter
# Security for LLM outputs
npm install dompurifyPerformance Optimization
Critical for smooth streaming:
// Memoize message rendering
const MemoizedMessage = memo(Message, (prev, next) =>
prev.content === next.content && prev.isStreaming === next.isStreaming
);
// Debounce streaming updates
const debouncedUpdate = useMemo(
() => debounce(updateMessage, 50),
[]
);
// Virtual scrolling for long conversations
import { VariableSizeList } from 'react-window';For detailed performance patterns, see references/streaming-ux.md.
Security Considerations
Always sanitize AI outputs:
import DOMPurify from 'dompurify';
function SafeAIContent({ content }) {
const sanitized = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['class']
});
return <Streamdown>{sanitized}</Streamdown>;
}Accessibility
Ensure AI chat is usable by everyone:
// ARIA live regions for screen readers
<div role="log" aria-live="polite" aria-relevant="additions">
{messages.map(msg => (
<article key={msg.id} role="article" aria-label={`${msg.role} message`}>
{msg.content}
</article>
))}
</div>
// Loading announcements
<div role="status" aria-live="polite" className="sr-only">
{isLoading ? 'AI is responding' : ''}
</div>For complete accessibility patterns, see references/accessibility.md.
Bundled Resources
Scripts (Token-Free Execution)
- Run
scripts/parse_stream.jsto parse incomplete markdown during streaming - Run
scripts/calculate_tokens.pyto estimate token usage and context limits - Run
scripts/format_messages.jsto format message history for export
References (Progressive Disclosure)
references/streaming-patterns.md- Complete streaming UX patternsreferences/context-management.md- Token limits and conversation strategiesreferences/multimodal-input.md- Image, file, and voice handlingreferences/feedback-loops.md- User feedback and RLHF patternsreferences/error-handling.md- AI-specific error scenariosreferences/tool-usage.md- Visualizing function calls and tool usereferences/accessibility-chat.md- Screen reader and keyboard supportreferences/library-guide.md- Detailed library documentationreferences/performance-optimization.md- Streaming performance patterns
Examples
examples/basic-chat.tsx- Minimal ChatGPT-style interfaceexamples/streaming-chat.tsx- Advanced streaming with memoizationexamples/multimodal-chat.tsx- Images and file uploadsexamples/code-assistant.tsx- IDE-style code copilotexamples/tool-calling-chat.tsx- Function calling visualization
Assets
assets/system-prompts.json- Curated prompts for different use casesassets/message-templates.json- Pre-built message componentsassets/error-messages.json- User-friendly error messagesassets/themes.json- Light, dark, and high-contrast themes
Design Token Integration
All visual styling uses the design-tokens system:
/* Message bubbles use design tokens */
.message.user {
background: var(--message-user-bg, var(--color-primary));
color: var(--message-user-text, var(--color-white));
padding: var(--message-padding, var(--spacing-md));
border-radius: var(--message-border-radius, var(--radius-lg));
}
.message.ai {
background: var(--message-ai-bg, var(--color-gray-100));
color: var(--message-ai-text, var(--color-text-primary));
}See skills/design-tokens/ for complete theming system.
Key Innovations
This skill provides industry-first solutions for:
- Memoized streaming rendering - 10-50x performance improvement
- Intelligent auto-scroll - User activity-aware scrolling
- Token metaphors - User-friendly context communication
- Incomplete markdown handling - Graceful partial rendering
- RLHF patterns - Effective feedback collection
- Conversation branching - Non-linear conversation trees
- Multi-modal integration - Seamless file/image/voice handling
- Accessibility-first - Built-in screen reader support
Strategic Importance
This is THE most critical skill because:
1. Perfect timing - Every app adding AI (2024-2025 boom) 2. No standards exist - Opportunity to define patterns 3. Meta-advantage - Building WITH Claude = intimate UX knowledge 4. Unique challenges - Streaming, context, hallucinations all new 5. Reference implementation - Can become the standard others follow
Master this skill to lead the AI interface revolution.
{
"errors": {
"network": {
"connection_failed": {
"code": "NETWORK_ERROR",
"title": "Connection Failed",
"message": "Unable to connect to the AI service. Please check your internet connection and try again.",
"suggestion": "Check if you're connected to the internet. If the problem persists, the service might be temporarily unavailable.",
"recovery": ["retry", "check_status", "offline_mode"],
"severity": "error"
},
"timeout": {
"code": "TIMEOUT_ERROR",
"title": "Request Timed Out",
"message": "The request took too long to complete. The AI service might be experiencing high load.",
"suggestion": "Try again in a moment. If you're sending a complex request, consider breaking it into smaller parts.",
"recovery": ["retry", "simplify", "wait"],
"severity": "warning"
},
"offline": {
"code": "OFFLINE",
"title": "You're Offline",
"message": "No internet connection detected. Your messages will be sent when you're back online.",
"suggestion": "Check your internet connection. Messages are saved locally and will be sent automatically when connection is restored.",
"recovery": ["queue", "check_connection"],
"severity": "info"
}
},
"ai_service": {
"refusal": {
"code": "AI_REFUSAL",
"title": "Request Cannot Be Completed",
"message": "I'm unable to help with this request due to safety or policy constraints.",
"suggestion": "Try rephrasing your request or asking for something related but within guidelines.",
"recovery": ["rephrase", "alternatives"],
"severity": "info"
},
"hallucination_warning": {
"code": "LOW_CONFIDENCE",
"title": "Low Confidence Response",
"message": "This response may contain inaccuracies. Please verify important information from authoritative sources.",
"suggestion": "Cross-reference critical information, especially facts, figures, and technical details.",
"recovery": ["verify", "request_sources"],
"severity": "warning"
},
"capability_limit": {
"code": "CAPABILITY_LIMIT",
"title": "Beyond Current Capabilities",
"message": "This request is outside my current capabilities or training.",
"suggestion": "I can help with related topics or break this down into manageable parts.",
"recovery": ["simplify", "alternatives"],
"severity": "info"
},
"model_unavailable": {
"code": "MODEL_UNAVAILABLE",
"title": "AI Model Unavailable",
"message": "The selected AI model is temporarily unavailable.",
"suggestion": "Try using an alternative model or wait a few moments before retrying.",
"recovery": ["switch_model", "retry", "wait"],
"severity": "warning"
}
},
"rate_limiting": {
"rate_limit_exceeded": {
"code": "RATE_LIMIT",
"title": "Rate Limit Reached",
"message": "You've exceeded the maximum number of requests. Please wait before sending more messages.",
"suggestion": "Take a break and return in a few minutes. Consider upgrading your plan for higher limits.",
"recovery": ["wait", "upgrade", "queue"],
"severity": "warning"
},
"daily_limit": {
"code": "DAILY_LIMIT",
"title": "Daily Limit Reached",
"message": "You've reached your daily message limit.",
"suggestion": "Your limit will reset tomorrow. Consider upgrading for unlimited access.",
"recovery": ["wait_daily", "upgrade"],
"severity": "info"
},
"token_limit": {
"code": "TOKEN_LIMIT",
"title": "Message Too Long",
"message": "Your message exceeds the maximum length allowed.",
"suggestion": "Please shorten your message or split it into multiple smaller messages.",
"recovery": ["shorten", "split"],
"severity": "error"
}
},
"context": {
"context_overflow": {
"code": "CONTEXT_OVERFLOW",
"title": "Conversation Too Long",
"message": "This conversation has exceeded the maximum context length.",
"suggestion": "Start a new conversation or let me summarize the discussion so far to continue.",
"recovery": ["summarize", "new_conversation", "export"],
"severity": "warning"
},
"context_lost": {
"code": "CONTEXT_LOST",
"title": "Context Lost",
"message": "I've lost track of our earlier conversation.",
"suggestion": "Please remind me what we were discussing or start fresh.",
"recovery": ["recap", "new_conversation"],
"severity": "error"
},
"memory_limit": {
"code": "MEMORY_LIMIT",
"title": "Memory Limit Reached",
"message": "Cannot store more conversation history.",
"suggestion": "Clear old conversations or export them for backup.",
"recovery": ["clear_history", "export"],
"severity": "warning"
}
},
"input_validation": {
"empty_message": {
"code": "EMPTY_INPUT",
"title": "Empty Message",
"message": "Please type a message before sending.",
"suggestion": "Enter your question or request in the input field.",
"recovery": ["type_message"],
"severity": "info"
},
"invalid_format": {
"code": "INVALID_FORMAT",
"title": "Invalid Format",
"message": "The message format is not supported.",
"suggestion": "Please use text, or supported file formats for attachments.",
"recovery": ["change_format", "remove_attachment"],
"severity": "error"
},
"file_too_large": {
"code": "FILE_TOO_LARGE",
"title": "File Too Large",
"message": "The attached file exceeds the maximum size limit.",
"suggestion": "Please use a smaller file or compress it before uploading.",
"recovery": ["compress", "reduce_size"],
"severity": "error"
},
"unsupported_file": {
"code": "UNSUPPORTED_FILE",
"title": "Unsupported File Type",
"message": "This file type is not supported.",
"suggestion": "Supported formats include images (PNG, JPG), documents (PDF, TXT), and code files.",
"recovery": ["convert", "change_file"],
"severity": "error"
}
},
"authentication": {
"unauthorized": {
"code": "UNAUTHORIZED",
"title": "Authentication Required",
"message": "Please sign in to continue using the chat.",
"suggestion": "Your session may have expired. Please sign in again.",
"recovery": ["sign_in", "refresh"],
"severity": "error"
},
"forbidden": {
"code": "FORBIDDEN",
"title": "Access Denied",
"message": "You don't have permission to perform this action.",
"suggestion": "Contact support if you believe this is an error.",
"recovery": ["contact_support", "check_permissions"],
"severity": "error"
},
"session_expired": {
"code": "SESSION_EXPIRED",
"title": "Session Expired",
"message": "Your session has expired for security reasons.",
"suggestion": "Please refresh the page and sign in again.",
"recovery": ["refresh", "sign_in"],
"severity": "warning"
}
},
"server": {
"internal_error": {
"code": "INTERNAL_ERROR",
"title": "Something Went Wrong",
"message": "An unexpected error occurred on our end.",
"suggestion": "We're working to fix this. Please try again in a moment.",
"recovery": ["retry", "report", "status_page"],
"severity": "error"
},
"maintenance": {
"code": "MAINTENANCE",
"title": "Under Maintenance",
"message": "The service is temporarily unavailable for scheduled maintenance.",
"suggestion": "We'll be back shortly. Check our status page for updates.",
"recovery": ["wait", "status_page"],
"severity": "info"
},
"degraded_performance": {
"code": "DEGRADED",
"title": "Slower Than Usual",
"message": "The service is experiencing slower response times.",
"suggestion": "Responses may take longer than usual. Thank you for your patience.",
"recovery": ["wait", "retry_later"],
"severity": "warning"
}
}
},
"recovery_actions": {
"retry": {
"label": "Try Again",
"description": "Attempt the request again"
},
"check_status": {
"label": "Check Status",
"description": "View service status page"
},
"offline_mode": {
"label": "Work Offline",
"description": "Continue with limited functionality"
},
"rephrase": {
"label": "Rephrase",
"description": "Try asking in a different way"
},
"alternatives": {
"label": "Alternatives",
"description": "Explore related options"
},
"verify": {
"label": "Verify",
"description": "Check information from other sources"
},
"request_sources": {
"label": "Request Sources",
"description": "Ask for citations and references"
},
"simplify": {
"label": "Simplify",
"description": "Break down into smaller requests"
},
"switch_model": {
"label": "Switch Model",
"description": "Use a different AI model"
},
"wait": {
"label": "Wait",
"description": "Try again in a few moments"
},
"upgrade": {
"label": "Upgrade",
"description": "Get higher limits with a paid plan"
},
"queue": {
"label": "Queue Message",
"description": "Send automatically when available"
},
"shorten": {
"label": "Shorten",
"description": "Reduce message length"
},
"split": {
"label": "Split Message",
"description": "Divide into multiple messages"
},
"summarize": {
"label": "Summarize",
"description": "Compress conversation history"
},
"new_conversation": {
"label": "New Chat",
"description": "Start a fresh conversation"
},
"export": {
"label": "Export",
"description": "Save conversation for backup"
},
"clear_history": {
"label": "Clear History",
"description": "Remove old conversations"
},
"sign_in": {
"label": "Sign In",
"description": "Authenticate to continue"
},
"refresh": {
"label": "Refresh Page",
"description": "Reload the application"
},
"contact_support": {
"label": "Contact Support",
"description": "Get help from our team"
},
"report": {
"label": "Report Issue",
"description": "Let us know about this problem"
}
},
"user_friendly_messages": {
"thinking": [
"Let me think about that...",
"Processing your request...",
"Working on it...",
"Just a moment...",
"Analyzing..."
],
"still_thinking": [
"This is taking a bit longer...",
"Still working on it...",
"Almost there...",
"Complex request, bear with me...",
"Taking extra care with this one..."
],
"success": [
"Got it!",
"Here's what I found:",
"I can help with that:",
"Great question! Here's my response:",
"Thanks for your patience. Here's the answer:"
],
"partial_success": [
"I found some information, though it may not be complete:",
"Here's what I could determine:",
"I have a partial answer:",
"Based on available information:"
],
"retry_success": [
"Success on retry!",
"Got through this time!",
"Connection restored!",
"Back online!",
"We're back in business!"
]
}
}{
"message_templates": {
"welcome": {
"first_time": {
"title": "Welcome Message - First Time User",
"template": "Hello! I'm your AI assistant. I'm here to help with questions, creative tasks, analysis, coding, and much more. Feel free to ask me anything or explore what I can do. How can I assist you today?"
},
"returning": {
"title": "Welcome Back Message",
"template": "Welcome back! Ready to continue our conversation or start something new?"
},
"morning": {
"title": "Morning Greeting",
"template": "Good morning! Fresh start to the day. What can I help you with?"
},
"afternoon": {
"title": "Afternoon Greeting",
"template": "Good afternoon! How can I assist you today?"
},
"evening": {
"title": "Evening Greeting",
"template": "Good evening! What would you like to work on?"
}
},
"feedback_requests": {
"after_complex": {
"title": "After Complex Response",
"template": "That was quite detailed! Let me know if you need clarification on any part or would like me to elaborate on specific aspects."
},
"after_code": {
"title": "After Code Generation",
"template": "I've provided the code above. Would you like me to explain any part in more detail, add comments, or help you adapt it to your specific use case?"
},
"after_list": {
"title": "After List or Options",
"template": "These are the options I've identified. Would you like to explore any of these in more depth?"
},
"need_more_info": {
"title": "Need More Information",
"template": "I'd be happy to help, but I need a bit more information to provide the best assistance. Could you tell me more about [specific aspect]?"
}
},
"transitions": {
"topic_change": {
"title": "Changing Topics",
"template": "I see we're shifting to a new topic. Let me adjust my focus to help you with this."
},
"deep_dive": {
"title": "Going Deeper",
"template": "Let's dive deeper into this. I'll provide more detailed information."
},
"summary_transition": {
"title": "Moving to Summary",
"template": "Let me summarize what we've discussed so far..."
},
"practical_application": {
"title": "To Practical Application",
"template": "Now let's look at how to apply this in practice..."
}
},
"clarifications": {
"ambiguous": {
"title": "Ambiguous Request",
"template": "Your question could be interpreted in a few different ways. Are you asking about [option A] or [option B]? Or perhaps something else?"
},
"assumptions": {
"title": "Making Assumptions",
"template": "I'm going to assume you mean [assumption]. Please let me know if you meant something different."
},
"technical_level": {
"title": "Technical Level Check",
"template": "Before I continue, would you prefer a technical deep-dive or a high-level overview?"
},
"scope_check": {
"title": "Scope Clarification",
"template": "Just to clarify the scope - are you looking for [specific aspect] or a broader view including [related aspects]?"
}
},
"explanations": {
"step_by_step": {
"title": "Step-by-Step Explanation",
"template": "Let me break this down step by step:\n\n1. [First step]\n2. [Second step]\n3. [Third step]\n\nEach step builds on the previous one to achieve [goal]."
},
"analogy": {
"title": "Explanation with Analogy",
"template": "Think of it like [analogy]. Just as [analogy explanation], similarly [concept explanation]."
},
"pros_cons": {
"title": "Pros and Cons Format",
"template": "Let me outline the pros and cons:\n\n**Pros:**\n- [Advantage 1]\n- [Advantage 2]\n\n**Cons:**\n- [Disadvantage 1]\n- [Disadvantage 2]\n\n**Recommendation:** [Based on the analysis]"
},
"comparison": {
"title": "Comparison Format",
"template": "Here's how these options compare:\n\n| Aspect | Option A | Option B |\n|--------|----------|----------|\n| [Aspect 1] | [A detail] | [B detail] |\n| [Aspect 2] | [A detail] | [B detail] |"
}
},
"code_templates": {
"function_template": {
"title": "Function Documentation",
"template": "```[language]\n/**\n * [Function description]\n * @param {[type]} [param] - [Description]\n * @returns {[type]} [Description]\n */\nfunction [name]([params]) {\n // Implementation\n}\n```"
},
"error_handling": {
"title": "Error Handling Pattern",
"template": "Here's how to handle potential errors:\n\n```[language]\ntry {\n // Your code here\n} catch (error) {\n // Error handling\n console.error('Error:', error);\n // Recovery or user feedback\n}\n```"
},
"before_after": {
"title": "Before/After Code",
"template": "**Before (Issue: [problem]):**\n```[language]\n[original code]\n```\n\n**After (Solution: [fix]):**\n```[language]\n[improved code]\n```\n\n**Key changes:**\n- [Change 1]\n- [Change 2]"
}
},
"conclusions": {
"summary": {
"title": "Summary Conclusion",
"template": "To summarize: [key points]. The main takeaway is [primary conclusion]."
},
"next_steps": {
"title": "Next Steps",
"template": "Now that we've covered [topic], your next steps could be:\n1. [Action 1]\n2. [Action 2]\n3. [Action 3]"
},
"open_ended": {
"title": "Open for Questions",
"template": "I hope this helps! Feel free to ask if you need clarification on any part or want to explore related topics."
},
"check_understanding": {
"title": "Understanding Check",
"template": "Does this explanation make sense? I'm happy to clarify any points or approach it from a different angle if needed."
}
},
"error_messages": {
"rate_limit": {
"title": "Rate Limit Message",
"template": "It looks like we've hit the rate limit. Please wait a moment before sending your next message. This helps ensure quality service for all users."
},
"context_limit": {
"title": "Context Limit Message",
"template": "Our conversation is getting quite long! To continue effectively, I'll need to summarize our earlier discussion. This helps me maintain context while staying within system limits."
},
"processing_error": {
"title": "Processing Error",
"template": "I encountered an issue processing your request. Please try rephrasing or breaking it into smaller parts. If the problem persists, it might be a temporary issue."
},
"connection_error": {
"title": "Connection Error",
"template": "There seems to be a connection issue. Your message has been saved and will be sent once the connection is restored."
}
},
"interactive": {
"choice_prompt": {
"title": "Multiple Choice",
"template": "I can help you with this in several ways:\n\nA) [Option A description]\nB) [Option B description]\nC) [Option C description]\n\nWhich approach would you prefer?"
},
"confirmation": {
"title": "Confirmation Request",
"template": "Just to confirm, you'd like me to [action]. Is that correct? Type 'yes' to proceed or let me know if you meant something else."
},
"iteration": {
"title": "Iteration Prompt",
"template": "Here's my first attempt. Would you like me to:\n- Refine this further\n- Try a different approach\n- Add more detail\n- Make it more concise"
}
}
},
"quick_replies": [
"Tell me more",
"Can you explain that differently?",
"Show me an example",
"What are the alternatives?",
"How does this work?",
"What's next?",
"Start over",
"Summarize this",
"Give me the key points",
"Is there a simpler way?"
],
"conversation_enders": [
"Thanks for the help!",
"That's all I needed",
"Perfect, thank you",
"This is very helpful",
"I understand now",
"Great explanation",
"Problem solved",
"See you later",
"I'll try this out",
"Much appreciated"
]
}{
"prompts": {
"default": {
"name": "General Assistant",
"prompt": "You are a helpful AI assistant. Provide clear, accurate, and helpful responses. Be concise but thorough."
},
"technical": {
"name": "Technical Assistant",
"prompt": "You are a technical AI assistant specializing in software development. Provide detailed technical explanations with code examples when appropriate. Focus on best practices and modern development patterns."
},
"creative": {
"name": "Creative Writer",
"prompt": "You are a creative writing assistant. Help with storytelling, content creation, and creative expression. Be imaginative and engaging while maintaining clarity."
},
"tutor": {
"name": "Educational Tutor",
"prompt": "You are an educational tutor. Break down complex concepts into understandable parts. Use examples and analogies. Encourage learning through questions and interactive exploration."
},
"business": {
"name": "Business Analyst",
"prompt": "You are a business analysis assistant. Focus on strategic thinking, data-driven insights, and practical business solutions. Provide actionable recommendations."
},
"research": {
"name": "Research Assistant",
"prompt": "You are a research assistant. Provide well-sourced information, cite references when possible, and maintain academic rigor. Distinguish between facts and speculation."
},
"coder": {
"name": "Code Assistant",
"prompt": "You are a coding assistant. Write clean, well-documented code following best practices. Explain your code and reasoning. Prioritize readability, maintainability, and performance."
},
"designer": {
"name": "Design Consultant",
"prompt": "You are a design consultant specializing in UI/UX. Focus on user experience, accessibility, and modern design principles. Provide practical design solutions with rationale."
},
"data": {
"name": "Data Analyst",
"prompt": "You are a data analysis assistant. Help with data interpretation, statistical analysis, and visualization recommendations. Explain findings clearly with appropriate context."
},
"support": {
"name": "Customer Support",
"prompt": "You are a friendly customer support assistant. Be patient, empathetic, and solution-oriented. Help users resolve issues step-by-step with clear instructions."
}
},
"customization": {
"temperature": {
"precise": 0.3,
"balanced": 0.7,
"creative": 0.9
},
"length": {
"concise": "Keep responses brief and to the point",
"detailed": "Provide comprehensive explanations with examples",
"variable": "Adjust response length based on question complexity"
},
"tone": {
"professional": "Maintain a formal, professional tone",
"friendly": "Be conversational and approachable",
"academic": "Use scholarly language and structure",
"casual": "Keep it light and informal"
},
"formatting": {
"structured": "Use clear headings, bullet points, and numbered lists",
"narrative": "Write in flowing paragraphs",
"code-heavy": "Emphasize code examples with minimal explanation",
"visual": "Include diagrams, tables, and visual representations where helpful"
}
},
"instructions": {
"code_review": "Review this code for bugs, performance issues, and best practice violations. Suggest improvements with explanations.",
"explain_concept": "Explain this concept as if teaching someone new to the field. Use analogies and build from fundamentals.",
"debug_help": "Help me debug this issue. Ask clarifying questions, suggest diagnostic steps, and provide potential solutions.",
"brainstorm": "Help me brainstorm ideas. Be creative and explore various angles. Build on concepts collaboratively.",
"summarize": "Provide a concise summary highlighting key points, main arguments, and important conclusions.",
"critique": "Provide constructive criticism. Identify strengths and weaknesses. Suggest specific improvements.",
"translate": "Translate this content while preserving meaning, context, and appropriate cultural adaptations.",
"optimize": "Optimize this for better performance, readability, or user experience. Explain your optimizations.",
"document": "Create comprehensive documentation including purpose, usage, examples, and important notes.",
"test": "Create test cases covering normal operations, edge cases, and error conditions. Include expected outcomes."
},
"conversation_starters": [
"How can I help you today?",
"What would you like to explore?",
"I'm here to assist. What's on your mind?",
"Let's tackle your challenge together. What are you working on?",
"Ready to help! What questions do you have?"
],
"error_responses": {
"unclear_request": "I'm not quite sure what you're asking. Could you provide more details or rephrase your question?",
"too_broad": "That's a broad topic. Could you be more specific about what aspect you'd like to explore?",
"need_context": "I need more context to provide a helpful response. Can you share additional background information?",
"capability_limit": "I'm not able to help with that specific request, but I can suggest alternatives or related topics I can assist with.",
"sensitive_topic": "I understand this is important, but I need to approach this topic carefully. Let me provide information that's helpful while being responsible."
}
}{
"themes": {
"light": {
"name": "Light Mode",
"description": "Clean, bright interface for daytime use",
"tokens": {
"chat-bg": "#f5f5f5",
"message-user-bg": "#007AFF",
"message-user-text": "#ffffff",
"message-ai-bg": "#ffffff",
"message-ai-text": "#000000",
"message-system-bg": "#FFF3CD",
"message-system-text": "#856404",
"input-bg": "#ffffff",
"input-border": "#e0e0e0",
"input-border-focus": "#007AFF",
"chat-border-color": "#e0e0e0",
"code-block-bg": "#2d2d2d",
"code-text-color": "#f8f8f2",
"typing-indicator-color": "#666666",
"cursor-color": "#000000",
"message-padding": "12px 16px",
"message-gap": "12px",
"message-border-radius": "16px",
"message-shadow": "0 1px 2px rgba(0,0,0,0.1)"
}
},
"dark": {
"name": "Dark Mode",
"description": "Eye-friendly dark theme for extended conversations",
"tokens": {
"chat-bg": "#1a1a1a",
"message-user-bg": "#0066CC",
"message-user-text": "#ffffff",
"message-ai-bg": "#2d2d2d",
"message-ai-text": "#e0e0e0",
"message-system-bg": "#3d3d00",
"message-system-text": "#ffff99",
"input-bg": "#2d2d2d",
"input-border": "#404040",
"input-border-focus": "#0066CC",
"chat-border-color": "#404040",
"code-block-bg": "#000000",
"code-text-color": "#f8f8f2",
"typing-indicator-color": "#999999",
"cursor-color": "#ffffff",
"message-padding": "12px 16px",
"message-gap": "12px",
"message-border-radius": "16px",
"message-shadow": "0 2px 4px rgba(0,0,0,0.5)"
}
},
"high-contrast": {
"name": "High Contrast",
"description": "Maximum readability with strong contrast",
"tokens": {
"chat-bg": "#000000",
"message-user-bg": "#0000FF",
"message-user-text": "#FFFFFF",
"message-ai-bg": "#FFFFFF",
"message-ai-text": "#000000",
"message-system-bg": "#FFFF00",
"message-system-text": "#000000",
"input-bg": "#000000",
"input-border": "#FFFFFF",
"input-border-focus": "#00FF00",
"chat-border-color": "#FFFFFF",
"code-block-bg": "#000000",
"code-text-color": "#00FF00",
"typing-indicator-color": "#FFFFFF",
"cursor-color": "#00FF00",
"message-padding": "16px 20px",
"message-gap": "16px",
"message-border-radius": "8px",
"message-shadow": "none",
"message-border": "2px solid currentColor"
}
},
"sepia": {
"name": "Sepia",
"description": "Warm, comfortable theme for long reading sessions",
"tokens": {
"chat-bg": "#f4ecd8",
"message-user-bg": "#8b4513",
"message-user-text": "#f4ecd8",
"message-ai-bg": "#ffffff",
"message-ai-text": "#5d4037",
"message-system-bg": "#fff8dc",
"message-system-text": "#704214",
"input-bg": "#ffffff",
"input-border": "#d2b48c",
"input-border-focus": "#8b4513",
"chat-border-color": "#d2b48c",
"code-block-bg": "#3e2723",
"code-text-color": "#ffecb3",
"typing-indicator-color": "#8b6914",
"cursor-color": "#5d4037",
"message-padding": "12px 16px",
"message-gap": "12px",
"message-border-radius": "12px",
"message-shadow": "0 1px 3px rgba(139,69,19,0.2)"
}
},
"ocean": {
"name": "Ocean",
"description": "Calming blue-green theme",
"tokens": {
"chat-bg": "#e0f2f1",
"message-user-bg": "#00695c",
"message-user-text": "#ffffff",
"message-ai-bg": "#ffffff",
"message-ai-text": "#004d40",
"message-system-bg": "#b2dfdb",
"message-system-text": "#00695c",
"input-bg": "#ffffff",
"input-border": "#80cbc4",
"input-border-focus": "#00695c",
"chat-border-color": "#80cbc4",
"code-block-bg": "#004d40",
"code-text-color": "#b2dfdb",
"typing-indicator-color": "#00897b",
"cursor-color": "#004d40",
"message-padding": "12px 16px",
"message-gap": "12px",
"message-border-radius": "16px",
"message-shadow": "0 2px 4px rgba(0,77,64,0.15)"
}
},
"sunset": {
"name": "Sunset",
"description": "Warm orange and purple gradient theme",
"tokens": {
"chat-bg": "linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)",
"message-user-bg": "#764ba2",
"message-user-text": "#ffffff",
"message-ai-bg": "rgba(255,255,255,0.95)",
"message-ai-text": "#4a148c",
"message-system-bg": "rgba(245,147,251,0.3)",
"message-system-text": "#4a148c",
"input-bg": "rgba(255,255,255,0.95)",
"input-border": "rgba(118,75,162,0.3)",
"input-border-focus": "#764ba2",
"chat-border-color": "rgba(118,75,162,0.2)",
"code-block-bg": "#4a148c",
"code-text-color": "#f3e5f5",
"typing-indicator-color": "#764ba2",
"cursor-color": "#764ba2",
"message-padding": "14px 18px",
"message-gap": "14px",
"message-border-radius": "20px",
"message-shadow": "0 4px 6px rgba(118,75,162,0.2)"
}
},
"minimal": {
"name": "Minimal",
"description": "Ultra-clean with minimal visual elements",
"tokens": {
"chat-bg": "#ffffff",
"message-user-bg": "transparent",
"message-user-text": "#000000",
"message-ai-bg": "transparent",
"message-ai-text": "#000000",
"message-system-bg": "transparent",
"message-system-text": "#666666",
"input-bg": "#ffffff",
"input-border": "#000000",
"input-border-focus": "#000000",
"chat-border-color": "#000000",
"code-block-bg": "#f5f5f5",
"code-text-color": "#000000",
"typing-indicator-color": "#000000",
"cursor-color": "#000000",
"message-padding": "8px 0",
"message-gap": "24px",
"message-border-radius": "0",
"message-shadow": "none",
"message-border-bottom": "1px solid #e0e0e0",
"message-user-align": "right",
"message-ai-align": "left"
}
},
"terminal": {
"name": "Terminal",
"description": "Classic terminal/console appearance",
"tokens": {
"chat-bg": "#000000",
"message-user-bg": "transparent",
"message-user-text": "#00ff00",
"message-ai-bg": "transparent",
"message-ai-text": "#00ff00",
"message-system-bg": "transparent",
"message-system-text": "#ffff00",
"input-bg": "#000000",
"input-border": "#00ff00",
"input-border-focus": "#00ffff",
"chat-border-color": "#00ff00",
"code-block-bg": "#0a0a0a",
"code-text-color": "#00ff00",
"typing-indicator-color": "#00ff00",
"cursor-color": "#00ff00",
"message-padding": "4px 0",
"message-gap": "8px",
"message-border-radius": "0",
"message-shadow": "none",
"font-family": "'Courier New', monospace",
"message-prefix-user": "> ",
"message-prefix-ai": "$ "
}
},
"forest": {
"name": "Forest",
"description": "Natural green theme inspired by nature",
"tokens": {
"chat-bg": "#f1f8e9",
"message-user-bg": "#2e7d32",
"message-user-text": "#ffffff",
"message-ai-bg": "#ffffff",
"message-ai-text": "#1b5e20",
"message-system-bg": "#dcedc8",
"message-system-text": "#33691e",
"input-bg": "#ffffff",
"input-border": "#81c784",
"input-border-focus": "#2e7d32",
"chat-border-color": "#81c784",
"code-block-bg": "#1b5e20",
"code-text-color": "#c8e6c9",
"typing-indicator-color": "#4caf50",
"cursor-color": "#2e7d32",
"message-padding": "12px 16px",
"message-gap": "12px",
"message-border-radius": "14px",
"message-shadow": "0 2px 4px rgba(46,125,50,0.15)"
}
},
"corporate": {
"name": "Corporate",
"description": "Professional business theme",
"tokens": {
"chat-bg": "#fafafa",
"message-user-bg": "#1565c0",
"message-user-text": "#ffffff",
"message-ai-bg": "#ffffff",
"message-ai-text": "#212121",
"message-system-bg": "#e3f2fd",
"message-system-text": "#0d47a1",
"input-bg": "#ffffff",
"input-border": "#bdbdbd",
"input-border-focus": "#1565c0",
"chat-border-color": "#e0e0e0",
"code-block-bg": "#263238",
"code-text-color": "#cfd8dc",
"typing-indicator-color": "#757575",
"cursor-color": "#212121",
"message-padding": "10px 14px",
"message-gap": "10px",
"message-border-radius": "4px",
"message-shadow": "0 1px 3px rgba(0,0,0,0.12)"
}
}
},
"color_schemes": {
"primary_colors": {
"blue": "#007AFF",
"green": "#34C759",
"purple": "#AF52DE",
"red": "#FF3B30",
"orange": "#FF9500",
"teal": "#00BCD4",
"indigo": "#5856D6",
"pink": "#FF2D55"
},
"neutral_colors": {
"gray-50": "#FAFAFA",
"gray-100": "#F5F5F5",
"gray-200": "#EEEEEE",
"gray-300": "#E0E0E0",
"gray-400": "#BDBDBD",
"gray-500": "#9E9E9E",
"gray-600": "#757575",
"gray-700": "#616161",
"gray-800": "#424242",
"gray-900": "#212121"
}
},
"animation_themes": {
"smooth": {
"message-transition": "all 0.3s ease-out",
"typing-animation-duration": "1.4s",
"scroll-behavior": "smooth"
},
"instant": {
"message-transition": "none",
"typing-animation-duration": "0s",
"scroll-behavior": "auto"
},
"bouncy": {
"message-transition": "all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55)",
"typing-animation-duration": "1s",
"scroll-behavior": "smooth"
}
}
}/**
* Basic ChatGPT-style interface implementation
* Uses Vercel AI SDK for streaming responses
*/
import React from 'react';
import { useChat } from 'ai/react';
import { Streamdown } from '@vercel/streamdown';
export function BasicChat() {
const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
error,
reload,
stop
} = useChat({
api: '/api/chat',
onFinish: (message) => {
console.log('Message complete:', message);
},
onError: (error) => {
console.error('Chat error:', error);
}
});
return (
<div className="chat-container">
{/* Header */}
<header className="chat-header">
<h1>AI Assistant</h1>
<span className="status">
{isLoading ? 'AI is thinking...' : 'Ready'}
</span>
</header>
{/* Messages */}
<div className="messages-container">
{messages.length === 0 && (
<div className="empty-state">
<p>Start a conversation by typing a message below</p>
</div>
)}
{messages.map((message) => (
<Message key={message.id} message={message} />
))}
{/* Scroll anchor */}
<div className="scroll-anchor" />
</div>
{/* Error display */}
{error && (
<div className="error-banner">
<span>Error: {error.message}</span>
<button onClick={reload} className="retry-btn">
Retry
</button>
</div>
)}
{/* Input form */}
<form onSubmit={handleSubmit} className="input-form">
<div className="input-container">
<textarea
value={input}
onChange={handleInputChange}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as any);
}
}}
placeholder="Type your message... (Shift+Enter for new line)"
disabled={isLoading}
rows={1}
className="message-input"
/>
{isLoading ? (
<button type="button" onClick={stop} className="stop-btn">
<StopIcon />
Stop
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="send-btn"
>
<SendIcon />
Send
</button>
)}
</div>
<div className="input-hints">
<span>Press Enter to send, Shift+Enter for new line</span>
</div>
</form>
</div>
);
}
/**
* Individual message component
*/
function Message({ message }: { message: any }) {
const isUser = message.role === 'user';
const isAssistant = message.role === 'assistant';
return (
<div className={`message ${message.role}`}>
{/* Avatar */}
<div className="message-avatar">
{isUser ? <UserIcon /> : isAssistant ? <BotIcon /> : <SystemIcon />}
</div>
{/* Content */}
<div className="message-content">
{/* Role label */}
<div className="message-role">
{isUser ? 'You' : isAssistant ? 'Assistant' : 'System'}
</div>
{/* Message text with markdown support */}
<div className="message-text">
<Streamdown>{message.content}</Streamdown>
</div>
{/* Timestamp */}
<div className="message-time">
{formatTime(message.createdAt)}
</div>
</div>
{/* Actions */}
{isAssistant && (
<MessageActions message={message} />
)}
</div>
);
}
/**
* Message action buttons
*/
function MessageActions({ message }: { message: any }) {
const [feedback, setFeedback] = React.useState<'positive' | 'negative' | null>(null);
const [copied, setCopied] = React.useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleFeedback = (type: 'positive' | 'negative') => {
setFeedback(feedback === type ? null : type);
// Send feedback to backend
sendFeedback(message.id, type);
};
return (
<div className="message-actions">
<button
onClick={() => handleFeedback('positive')}
className={`action-btn ${feedback === 'positive' ? 'selected' : ''}`}
aria-label="Good response"
>
<ThumbsUpIcon />
</button>
<button
onClick={() => handleFeedback('negative')}
className={`action-btn ${feedback === 'negative' ? 'selected' : ''}`}
aria-label="Bad response"
>
<ThumbsDownIcon />
</button>
<button
onClick={handleCopy}
className="action-btn"
aria-label="Copy to clipboard"
>
{copied ? <CheckIcon /> : <CopyIcon />}
</button>
</div>
);
}
/**
* Icon components
*/
const SendIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const StopIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="6" width="12" height="12" rx="2"/>
</svg>
);
const UserIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z" strokeWidth="2"/>
</svg>
);
const BotIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="11" width="18" height="10" rx="2" ry="2" strokeWidth="2"/>
<circle cx="12" cy="5" r="2" strokeWidth="2"/>
<path d="M12 7v4" strokeWidth="2"/>
</svg>
);
const SystemIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="10" strokeWidth="2"/>
<path d="M12 8v4M12 16h.01" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
const ThumbsUpIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" strokeWidth="2"/>
</svg>
);
const ThumbsDownIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" strokeWidth="2"/>
</svg>
);
const CopyIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" strokeWidth="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" strokeWidth="2"/>
</svg>
);
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="20 6 9 17 4 12" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
/**
* Utility functions
*/
function formatTime(date: Date | string) {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
async function sendFeedback(messageId: string, type: 'positive' | 'negative') {
try {
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messageId, type })
});
} catch (error) {
console.error('Failed to send feedback:', error);
}
}
/**
* Styles (using CSS modules or styled-components in production)
*/
const styles = `
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background: var(--chat-bg, #f5f5f5);
}
.chat-header {
padding: 1rem;
background: var(--header-bg, white);
border-bottom: 1px solid var(--border-color, #e0e0e0);
display: flex;
justify-content: space-between;
align-items: center;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 1rem;
scroll-behavior: smooth;
}
.empty-state {
text-align: center;
color: var(--text-secondary, #666);
margin-top: 2rem;
}
.message {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
animation: slideIn 0.3s ease-out;
}
.message.user {
flex-direction: row-reverse;
}
.message.user .message-content {
background: var(--message-user-bg, #007AFF);
color: var(--message-user-text, white);
}
.message.assistant .message-content {
background: var(--message-ai-bg, white);
color: var(--message-ai-text, black);
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--avatar-bg, #e0e0e0);
}
.message-content {
max-width: 70%;
padding: 0.75rem 1rem;
border-radius: var(--message-radius, 12px);
box-shadow: var(--message-shadow, 0 1px 2px rgba(0,0,0,0.1));
}
.message-role {
font-size: 0.75rem;
opacity: 0.7;
margin-bottom: 0.25rem;
}
.message-time {
font-size: 0.75rem;
opacity: 0.5;
margin-top: 0.25rem;
}
.message-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.action-btn {
padding: 0.25rem;
background: none;
border: none;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.2s;
}
.action-btn:hover {
opacity: 1;
}
.action-btn.selected {
opacity: 1;
color: var(--primary-color, #007AFF);
}
.input-form {
padding: 1rem;
background: var(--input-bg, white);
border-top: 1px solid var(--border-color, #e0e0e0);
}
.input-container {
display: flex;
gap: 0.5rem;
}
.message-input {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--input-border, #e0e0e0);
border-radius: var(--input-radius, 8px);
resize: none;
font-family: inherit;
}
.message-input:focus {
outline: none;
border-color: var(--primary-color, #007AFF);
}
.send-btn, .stop-btn {
padding: 0.75rem 1.5rem;
background: var(--primary-color, #007AFF);
color: white;
border: none;
border-radius: var(--button-radius, 8px);
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.stop-btn {
background: var(--danger-color, #dc3545);
}
.error-banner {
padding: 1rem;
background: var(--error-bg, #fee);
color: var(--error-color, #c00);
display: flex;
justify-content: space-between;
align-items: center;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive design */
@media (max-width: 768px) {
.message-content {
max-width: 85%;
}
}
`;import React, { useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import ReactMarkdown from 'react-markdown';
/**
* Code Assistant Chat Example
*
* Specialized chat for code-related tasks with:
* - Syntax highlighting for code blocks
* - Copy code button
* - Language detection
* - File context (show current file)
* - Code suggestions
*/
interface Message {
role: 'user' | 'assistant';
content: string;
}
export function CodeAssistant() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [currentFile, setCurrentFile] = useState('app/main.py');
const sendMessage = async () => {
if (!input.trim()) return;
const userMsg: Message = { role: 'user', content: input };
setMessages((prev) => [...prev, userMsg]);
setInput('');
// Include file context
const context = `Current file: ${currentFile}\n\n${input}`;
// Simulate API call
const response = await mockCodeResponse(context);
setMessages((prev) => [...prev, { role: 'assistant', content: response }]);
};
return (
<div style={{ display: 'flex', height: '100vh' }}>
{/* Sidebar with file context */}
<div style={{ width: '250px', borderRight: '1px solid #e2e8f0', padding: '16px', backgroundColor: '#f9fafb' }}>
<h3 style={{ fontSize: '14px', fontWeight: 600, marginBottom: '12px' }}>Current File</h3>
<div style={{ padding: '8px', backgroundColor: '#fff', borderRadius: '6px', border: '1px solid #e2e8f0', fontFamily: 'monospace', fontSize: '13px' }}>
{currentFile}
</div>
<h3 style={{ fontSize: '14px', fontWeight: 600, marginTop: '24px', marginBottom: '12px' }}>Quick Actions</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<button style={{ padding: '8px', textAlign: 'left', fontSize: '13px', border: '1px solid #e2e8f0', borderRadius: '6px', backgroundColor: '#fff', cursor: 'pointer' }}>
💡 Explain this code
</button>
<button style={{ padding: '8px', textAlign: 'left', fontSize: '13px', border: '1px solid #e2e8f0', borderRadius: '6px', backgroundColor: '#fff', cursor: 'pointer' }}>
🐛 Debug this function
</button>
<button style={{ padding: '8px', textAlign: 'left', fontSize: '13px', border: '1px solid #e2e8f0', borderRadius: '6px', backgroundColor: '#fff', cursor: 'pointer' }}>
✨ Add docstring
</button>
<button style={{ padding: '8px', textAlign: 'left', fontSize: '13px', border: '1px solid #e2e8f0', borderRadius: '6px', backgroundColor: '#fff', cursor: 'pointer' }}>
🔄 Refactor code
</button>
</div>
</div>
{/* Chat area */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflowY: 'auto', padding: '16px' }}>
{messages.length === 0 && (
<div style={{ textAlign: 'center', padding: '48px', color: '#64748b' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>👨💻</div>
<h2 style={{ fontSize: '20px', fontWeight: 600, marginBottom: '8px' }}>Code Assistant</h2>
<p>Ask me to explain, debug, refactor, or generate code</p>
</div>
)}
{messages.map((msg, i) => (
<div
key={i}
style={{
marginBottom: '24px',
display: 'flex',
flexDirection: 'column',
alignItems: msg.role === 'user' ? 'flex-end' : 'flex-start',
}}
>
<div
style={{
maxWidth: '80%',
backgroundColor: msg.role === 'user' ? '#3b82f6' : '#fff',
color: msg.role === 'user' ? '#fff' : '#1f2937',
padding: msg.role === 'user' ? '12px 16px' : '0',
borderRadius: msg.role === 'user' ? '18px' : '0',
border: msg.role === 'assistant' ? '1px solid #e2e8f0' : 'none',
}}
>
{msg.role === 'assistant' ? (
<ReactMarkdown
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const code = String(children).replace(/\n$/, '');
return !inline ? (
<CodeBlock code={code} language={match ? match[1] : 'text'} />
) : (
<code
style={{
backgroundColor: '#f3f4f6',
padding: '2px 6px',
borderRadius: '4px',
fontSize: '0.9em',
fontFamily: 'monospace',
}}
>
{children}
</code>
);
},
}}
>
{msg.content}
</ReactMarkdown>
) : (
msg.content
)}
</div>
</div>
))}
</div>
{/* Input */}
<div style={{ padding: '16px', borderTop: '1px solid #e2e8f0', backgroundColor: '#fff' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder="Ask about code..."
style={{
flex: 1,
padding: '12px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
resize: 'none',
minHeight: '52px',
fontFamily: 'inherit',
}}
/>
<button
onClick={sendMessage}
disabled={!input.trim()}
style={{
padding: '12px 24px',
backgroundColor: input.trim() ? '#3b82f6' : '#e2e8f0',
color: '#fff',
border: 'none',
borderRadius: '12px',
cursor: input.trim() ? 'pointer' : 'not-allowed',
fontWeight: 600,
}}
>
Send
</button>
</div>
</div>
</div>
</div>
);
}
function CodeBlock({ code, language }: { code: string; language: string }) {
const [copied, setCopied] = useState(false);
return (
<div style={{ margin: '12px 0', borderRadius: '8px', overflow: 'hidden' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 16px', backgroundColor: '#1e293b', color: '#fff', fontSize: '13px' }}>
<span>{language}</span>
<button
onClick={() => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}}
style={{ background: 'none', border: 'none', color: '#fff', cursor: 'pointer', fontSize: '13px' }}
>
{copied ? '✓ Copied' : '📋 Copy'}
</button>
</div>
<SyntaxHighlighter language={language} style={vscDarkPlus} customStyle={{ margin: 0 }}>
{code}
</SyntaxHighlighter>
</div>
);
}
async function mockCodeResponse(prompt: string): Promise<string> {
return `Here's a Python function to solve that:
\`\`\`python
def fibonacci(n: int) -> int:
"""Calculate nth Fibonacci number using dynamic programming"""
if n <= 1:
return n
dp = [0, 1]
for i in range(2, n + 1):
dp.append(dp[i-1] + dp[i-2])
return dp[n]
\`\`\`
This implementation:
- Uses dynamic programming for O(n) time complexity
- Includes type hints for clarity
- Has a docstring explaining the function
- Handles edge cases (n <= 1)
Would you like me to add unit tests or optimize further?`;
}
export default StreamingChat;
import React, { useState } from 'react';
import { Camera, Paperclip, Mic, X } from 'lucide-react';
/**
* Multimodal Chat Example
*
* Chat interface supporting multiple input types:
* - Text messages
* - Image upload with preview
* - Voice input
* - File attachments
*
* For models like GPT-4 Vision, Claude 3, Gemini Pro Vision
*/
interface Attachment {
id: string;
type: 'image' | 'audio' | 'file';
url: string;
name: string;
preview?: string;
}
export function MultimodalChat() {
const [input, setInput] = useState('');
const [attachments, setAttachments] = useState<Attachment[]>([]);
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
alert('Please upload an image file');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const preview = e.target?.result as string;
setAttachments((prev) => [
...prev,
{
id: Date.now().toString(),
type: 'image',
url: preview,
name: file.name,
preview,
},
]);
};
reader.readAsDataURL(file);
};
const removeAttachment = (id: string) => {
setAttachments((prev) => prev.filter((a) => a.id !== id));
};
const sendMessage = async () => {
if (!input.trim() && attachments.length === 0) return;
const message = {
text: input,
attachments,
};
console.log('Sending multimodal message:', message);
// Send to API (GPT-4 Vision, Claude 3, etc.)
// const response = await fetch('/api/chat/multimodal', {
// method: 'POST',
// body: JSON.stringify(message),
// });
setInput('');
setAttachments([]);
};
return (
<div style={{ maxWidth: '800px', margin: '0 auto', height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<div style={{ padding: '16px', borderBottom: '1px solid #e2e8f0', backgroundColor: '#fff' }}>
<h1 style={{ margin: 0, fontSize: '20px', fontWeight: 600 }}>Multi-Modal AI Assistant</h1>
<p style={{ margin: '4px 0 0 0', fontSize: '14px', color: '#64748b' }}>Upload images, audio, or files</p>
</div>
{/* Messages area */}
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', backgroundColor: '#f9fafb' }}>
<div style={{ textAlign: 'center', padding: '48px', color: '#64748b' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🎨</div>
<p>Upload an image and ask me about it!</p>
</div>
</div>
{/* Attachment previews */}
{attachments.length > 0 && (
<div style={{ padding: '16px', borderTop: '1px solid #e2e8f0', backgroundColor: '#f9fafb' }}>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
{attachments.map((attachment) => (
<div
key={attachment.id}
style={{
position: 'relative',
width: '120px',
height: '120px',
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid #e2e8f0',
}}
>
{attachment.type === 'image' && (
<img
src={attachment.preview}
alt={attachment.name}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
)}
<button
onClick={() => removeAttachment(attachment.id)}
style={{
position: 'absolute',
top: '4px',
right: '4px',
width: '24px',
height: '24px',
borderRadius: '50%',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: '#fff',
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<X size={14} />
</button>
</div>
))}
</div>
</div>
)}
{/* Input area */}
<div style={{ padding: '16px', borderTop: '1px solid #e2e8f0', backgroundColor: '#fff' }}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end' }}>
{/* Attachment buttons */}
<div style={{ display: 'flex', gap: '4px' }}>
<input
type="file"
accept="image/*"
onChange={handleImageUpload}
id="image-upload"
style={{ display: 'none' }}
/>
<label
htmlFor="image-upload"
style={{
width: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '8px',
border: '1px solid #e2e8f0',
cursor: 'pointer',
backgroundColor: '#fff',
}}
>
<Camera size={20} color="#64748b" />
</label>
<button
style={{
width: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '8px',
border: '1px solid #e2e8f0',
cursor: 'pointer',
backgroundColor: '#fff',
}}
>
<Mic size={20} color="#64748b" />
</button>
<button
style={{
width: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '8px',
border: '1px solid #e2e8f0',
cursor: 'pointer',
backgroundColor: '#fff',
}}
>
<Paperclip size={20} color="#64748b" />
</button>
</div>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder={
attachments.length > 0
? 'Ask about the uploaded image...'
: 'Type a message or upload an image...'
}
style={{
flex: 1,
padding: '12px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
resize: 'none',
minHeight: '52px',
fontFamily: 'inherit',
}}
/>
<button
onClick={sendMessage}
disabled={!input.trim() && attachments.length === 0}
style={{
padding: '12px 24px',
backgroundColor: (input.trim() || attachments.length > 0) ? '#3b82f6' : '#e2e8f0',
color: '#fff',
border: 'none',
borderRadius: '12px',
cursor: (input.trim() || attachments.length > 0) ? 'pointer' : 'not-allowed',
fontWeight: 600,
height: '52px',
}}
>
Send
</button>
</div>
</div>
</div>
);
}
async function mockCodeResponse(prompt: string): Promise<string> {
return 'Mock response for code assistant';
}
export default MultimodalChat;
import React, { useState, useRef, useEffect } from 'react';
/**
* Streaming Chat Example
*
* Complete chat interface with SSE streaming, message history, and auto-scroll.
* Demonstrates best practices for streaming LLM responses.
*/
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
export function StreamingChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [currentResponse, setCurrentResponse] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const eventSourceRef = useRef<EventSource | null>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(scrollToBottom, [messages, currentResponse]);
const sendMessage = async () => {
if (!input.trim() || isStreaming) return;
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: input.trim(),
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setInput('');
setIsStreaming(true);
setCurrentResponse('');
// Start SSE stream
const es = new EventSource(
`/api/chat/stream?message=${encodeURIComponent(userMessage.content)}`
);
eventSourceRef.current = es;
es.addEventListener('token', (e) => {
const token = JSON.parse(e.data).token;
setCurrentResponse((prev) => prev + token);
});
es.addEventListener('done', () => {
setMessages((prev) => [
...prev,
{
id: (Date.now() + 1).toString(),
role: 'assistant',
content: currentResponse,
timestamp: new Date(),
},
]);
setCurrentResponse('');
setIsStreaming(false);
es.close();
});
es.onerror = () => {
console.error('SSE error');
setIsStreaming(false);
es.close();
};
};
const stopGeneration = () => {
eventSourceRef.current?.close();
if (currentResponse) {
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: 'assistant',
content: currentResponse + ' [stopped]',
timestamp: new Date(),
},
]);
}
setCurrentResponse('');
setIsStreaming(false);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', maxWidth: '800px', margin: '0 auto' }}>
{/* Header */}
<div style={{ padding: '16px', borderBottom: '1px solid #e2e8f0', backgroundColor: '#fff' }}>
<h1 style={{ margin: 0, fontSize: '20px', fontWeight: 600 }}>AI Assistant</h1>
</div>
{/* Messages */}
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', backgroundColor: '#f9fafb' }}>
{messages.map((msg) => (
<div
key={msg.id}
style={{
display: 'flex',
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
marginBottom: '16px',
}}
>
<div
style={{
maxWidth: '70%',
padding: '12px 16px',
borderRadius: '18px',
backgroundColor: msg.role === 'user' ? '#3b82f6' : '#fff',
color: msg.role === 'user' ? '#fff' : '#1f2937',
border: msg.role === 'assistant' ? '1px solid #e2e8f0' : 'none',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{msg.content}
</div>
</div>
))}
{/* Streaming message */}
{currentResponse && (
<div style={{ display: 'flex', marginBottom: '16px' }}>
<div
style={{
maxWidth: '70%',
padding: '12px 16px',
borderRadius: '18px',
backgroundColor: '#fff',
color: '#1f2937',
border: '1px solid #e2e8f0',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{currentResponse}
<span style={{ display: 'inline-block', width: '2px', height: '16px', backgroundColor: '#3b82f6', animation: 'blink 1s infinite', marginLeft: '2px' }} />
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div style={{ padding: '16px', borderTop: '1px solid #e2e8f0', backgroundColor: '#fff' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder="Type your message..."
disabled={isStreaming}
style={{
flex: 1,
padding: '12px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
resize: 'none',
minHeight: '52px',
maxHeight: '200px',
fontFamily: 'inherit',
}}
/>
{isStreaming ? (
<button
onClick={stopGeneration}
style={{
padding: '12px 24px',
backgroundColor: '#ef4444',
color: '#fff',
border: 'none',
borderRadius: '12px',
cursor: 'pointer',
fontWeight: 600,
}}
>
⬛ Stop
</button>
) : (
<button
onClick={sendMessage}
disabled={!input.trim()}
style={{
padding: '12px 24px',
backgroundColor: input.trim() ? '#3b82f6' : '#e2e8f0',
color: '#fff',
border: 'none',
borderRadius: '12px',
cursor: input.trim() ? 'pointer' : 'not-allowed',
fontWeight: 600,
}}
>
Send
</button>
)}
</div>
</div>
<style>{`
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
`}</style>
</div>
);
}
export default StreamingChat;
import React, { useState } from 'react';
import { Search, Calculator, Database, Code } from 'lucide-react';
/**
* Tool-Calling Chat Example
*
* AI chat with function calling capabilities:
* - Weather lookup
* - Calculator
* - Database query
* - Code execution
*
* Demonstrates OpenAI function calling / Anthropic tool use pattern.
*/
interface Tool {
name: string;
description: string;
icon: React.ReactNode;
}
const AVAILABLE_TOOLS: Tool[] = [
{ name: 'search_web', description: 'Search the web', icon: <Search size={16} /> },
{ name: 'calculate', description: 'Perform calculations', icon: <Calculator size={16} /> },
{ name: 'query_database', description: 'Query database', icon: <Database size={16} /> },
{ name: 'execute_code', description: 'Run Python code', icon: <Code size={16} /> },
];
interface Message {
role: 'user' | 'assistant' | 'tool';
content: string;
toolCall?: {
name: string;
arguments: any;
result?: any;
};
}
export function ToolCallingChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const sendMessage = async () => {
if (!input.trim()) return;
const userMsg: Message = { role: 'user', content: input };
setMessages((prev) => [...prev, userMsg]);
setInput('');
// Simulate AI deciding to use a tool
const needsTool = input.toLowerCase().includes('weather') ||
input.toLowerCase().includes('calculate');
if (needsTool) {
// AI decides to call a tool
const toolCall = {
name: input.includes('weather') ? 'get_weather' : 'calculate',
arguments: input.includes('weather')
? { location: 'San Francisco' }
: { expression: '15 * 23' },
};
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: `I'll use the ${toolCall.name} tool to answer that.`,
toolCall,
},
]);
// Execute tool
const result = await executeTool(toolCall.name, toolCall.arguments);
setMessages((prev) => [
...prev,
{
role: 'tool',
content: JSON.stringify(result, null, 2),
toolCall: { ...toolCall, result },
},
]);
// AI generates final response using tool result
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: generateResponseWithToolResult(toolCall.name, result),
},
]);
} else {
// Regular response without tools
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: 'This is a regular response without using any tools.',
},
]);
}
};
return (
<div style={{ maxWidth: '800px', margin: '0 auto', height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Header with available tools */}
<div style={{ padding: '16px', borderBottom: '1px solid #e2e8f0', backgroundColor: '#fff' }}>
<h1 style={{ margin: '0 0 8px 0', fontSize: '20px', fontWeight: 600 }}>AI Assistant with Tools</h1>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{AVAILABLE_TOOLS.map((tool) => (
<div
key={tool.name}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 12px',
backgroundColor: '#f3f4f6',
borderRadius: '12px',
fontSize: '12px',
color: '#64748b',
}}
>
{tool.icon}
<span>{tool.description}</span>
</div>
))}
</div>
</div>
{/* Messages */}
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', backgroundColor: '#f9fafb' }}>
{messages.map((msg, i) => (
<div key={i} style={{ marginBottom: '16px' }}>
{msg.role === 'user' && (
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={{ maxWidth: '70%', padding: '12px 16px', borderRadius: '18px', backgroundColor: '#3b82f6', color: '#fff' }}>
{msg.content}
</div>
</div>
)}
{msg.role === 'assistant' && (
<div>
<div style={{ padding: '12px 16px', borderRadius: '18px', backgroundColor: '#fff', border: '1px solid #e2e8f0', maxWidth: '70%' }}>
{msg.content}
</div>
{msg.toolCall && !msg.toolCall.result && (
<div style={{ marginTop: '8px', padding: '8px 12px', backgroundColor: '#fef3c7', borderRadius: '8px', fontSize: '13px', display: 'inline-block' }}>
🔧 Calling tool: <code>{msg.toolCall.name}</code>
</div>
)}
</div>
)}
{msg.role === 'tool' && msg.toolCall && (
<div style={{ marginTop: '8px', padding: '12px', backgroundColor: '#f3f4f6', borderRadius: '8px', fontSize: '13px', fontFamily: 'monospace', maxWidth: '70%' }}>
<div style={{ fontWeight: 600, marginBottom: '8px', color: '#64748b' }}>
Tool Result: {msg.toolCall.name}
</div>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
{msg.content}
</pre>
</div>
)}
</div>
))}
</div>
{/* Attachment previews */}
{attachments.length > 0 && (
<div style={{ padding: '16px', borderTop: '1px solid #e2e8f0', backgroundColor: '#f9fafb' }}>
<div style={{ display: 'flex', gap: '8px' }}>
{attachments.map((att) => (
<div
key={att.id}
style={{
position: 'relative',
width: '80px',
height: '80px',
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid #e2e8f0',
}}
>
<img src={att.preview} alt={att.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<button
onClick={() => removeAttachment(att.id)}
style={{
position: 'absolute',
top: '4px',
right: '4px',
width: '20px',
height: '20px',
borderRadius: '50%',
backgroundColor: 'rgba(0,0,0,0.6)',
color: '#fff',
border: 'none',
cursor: 'pointer',
}}
>
<X size={12} />
</button>
</div>
))}
</div>
</div>
)}
{/* Input */}
<div style={{ padding: '16px', borderTop: '1px solid #e2e8f0', backgroundColor: '#fff' }}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end' }}>
<div style={{ display: 'flex', gap: '4px' }}>
<input type="file" accept="image/*" onChange={handleImageUpload} id="img-upload" style={{ display: 'none' }} />
<label htmlFor="img-upload" style={{ cursor: 'pointer', padding: '8px', borderRadius: '8px', border: '1px solid #e2e8f0' }}>
<Camera size={20} color="#64748b" />
</label>
</div>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder="Ask me to search, calculate, or query data..."
style={{ flex: 1, padding: '12px', borderRadius: '12px', border: '1px solid #e2e8f0', resize: 'none', minHeight: '52px' }}
/>
<button
onClick={sendMessage}
disabled={!input.trim() && attachments.length === 0}
style={{
padding: '12px 24px',
backgroundColor: (input.trim() || attachments.length > 0) ? '#3b82f6' : '#e2e8f0',
color: '#fff',
border: 'none',
borderRadius: '12px',
cursor: (input.trim() || attachments.length > 0) ? 'pointer' : 'not-allowed',
fontWeight: 600,
height: '52px',
}}
>
Send
</button>
</div>
</div>
</div>
);
}
async function executeTool(name: string, args: any): Promise<any> {
// Mock tool execution
const tools = {
get_weather: ({ location }: any) => ({
location,
temperature: 72,
condition: 'Sunny',
humidity: 45,
}),
calculate: ({ expression }: any) => ({
expression,
result: eval(expression),
}),
query_database: ({ query }: any) => ({
query,
results: [{ id: 1, name: 'Sample' }],
count: 1,
}),
};
return tools[name]?.(args) || { error: 'Tool not found' };
}
function generateResponseWithToolResult(toolName: string, result: any): string {
if (toolName === 'get_weather') {
return `The weather in ${result.location} is ${result.temperature}°F and ${result.condition}. Humidity is ${result.humidity}%.`;
}
if (toolName === 'calculate') {
return `The result of ${result.expression} is ${result.result}.`;
}
return JSON.stringify(result);
}
export default ToolCallingChat;
skill: "building-ai-chat"
version: "1.0"
domain: "frontend"
# Base outputs required for all AI chat implementations
base_outputs:
- path: "components/"
must_contain: ["ChatContainer", "MessageList", "InputArea"]
reason: "Core chat UI components directory"
- path: "components/ChatContainer.tsx"
must_contain: ["useChat", "messages", "input", "handleSubmit"]
reason: "Main chat interface container with streaming support"
- path: "components/MessageList.tsx"
must_contain: ["message.role", "message.content", "Streamdown"]
reason: "Message display with streaming markdown rendering"
- path: "components/InputArea.tsx"
must_contain: ["textarea", "onKeyDown", "Enter", "disabled"]
reason: "User input with keyboard shortcuts and state management"
- path: "lib/"
must_contain: ["chat-utils", "streaming"]
reason: "Utility functions for chat operations and streaming"
- path: "package.json"
must_contain: ["ai", "@ai-sdk/react", "@vercel/streamdown"]
reason: "Required AI chat dependencies"
# Conditional outputs based on configuration
conditional_outputs:
maturity:
starter:
- path: "components/BasicChat.tsx"
must_contain: ["useChat", "messages.map", "handleSubmit", "isLoading"]
reason: "Minimal ChatGPT-style interface (under 100 lines)"
- path: "components/Message.tsx"
must_contain: ["message.role", "Streamdown", "message.content"]
reason: "Basic message bubble with streaming support"
- path: "app/api/chat/route.ts"
must_contain: ["streamText", "openai", "messages"]
reason: "Server-side streaming endpoint"
- path: "styles/chat.css"
must_contain: [".message", ".user", ".assistant", ".input-form"]
reason: "Basic chat UI styling with message bubbles"
intermediate:
- path: "components/MessageList.tsx"
must_contain: ["useRef", "scrollIntoView", "messagesEndRef"]
reason: "Message list with auto-scroll behavior"
- path: "components/MessageActions.tsx"
must_contain: ["ThumbsUpIcon", "ThumbsDownIcon", "CopyIcon", "feedback"]
reason: "Message feedback controls (thumbs, copy, share)"
- path: "components/ResponseControls.tsx"
must_contain: ["stop", "regenerate", "continueGeneration", "editMessage"]
reason: "AI response controls (stop, regenerate, continue, edit)"
- path: "components/TokenIndicator.tsx"
must_contain: ["used", "total", "percentage", "progress-bar"]
reason: "Visual token usage and context limit display"
- path: "lib/auto-scroll.ts"
must_contain: ["shouldAutoScroll", "threshold", "scrollHeight"]
reason: "Smart auto-scroll logic (user activity-aware)"
- path: "lib/sanitize.ts"
must_contain: ["DOMPurify", "sanitize", "ALLOWED_TAGS"]
reason: "XSS protection for AI-generated content"
- path: "hooks/useChat.ts"
must_contain: ["useState", "useEffect", "messages", "isStreaming"]
reason: "Custom chat state management hook"
advanced:
- path: "components/MultiModalInput.tsx"
must_contain: ["FileInput", "ImagePreview", "VoiceInput", "attachments"]
reason: "Multi-modal input (text, images, files, voice)"
- path: "components/ToolUsageDisplay.tsx"
must_contain: ["tool.name", "tool.status", "tool.result", "Spinner"]
reason: "Visualization of AI tool/function calling"
- path: "components/ConversationBranching.tsx"
must_contain: ["branches", "activeThread", "switchBranch"]
reason: "Non-linear conversation tree navigation"
- path: "components/ErrorDisplay.tsx"
must_contain: ["error.code", "RATE_LIMIT", "refusal", "retryAfter"]
reason: "AI-specific error handling (refusals, rate limits, hallucinations)"
- path: "lib/context-management.ts"
must_contain: ["calculateTokens", "summarize", "pruneContext"]
reason: "Token counting and context window management"
- path: "lib/streaming-optimizations.ts"
must_contain: ["memo", "debounce", "useMemo", "updateMessage"]
reason: "Performance optimizations for streaming (memoization, debouncing)"
- path: "lib/voice-integration.ts"
must_contain: ["SpeechRecognition", "startRecording", "stopRecording"]
reason: "Web Speech API integration for voice input"
- path: "hooks/useVirtualScroll.ts"
must_contain: ["VariableSizeList", "react-window"]
reason: "Virtual scrolling for long conversation histories"
- path: "components/ChatAccessibility.tsx"
must_contain: ["role=\"log\"", "aria-live", "aria-relevant", "sr-only"]
reason: "Screen reader support and ARIA live regions"
- path: "tests/chat.test.tsx"
must_contain: ["render", "fireEvent", "waitFor", "mockUseChat"]
reason: "Unit tests for chat components and hooks"
frontend_framework:
react:
- path: "components/ChatContainer.tsx"
must_contain: ["import React", "useChat", "export function"]
reason: "React-based chat container"
- path: "hooks/useChat.ts"
must_contain: ["useState", "useEffect", "useCallback"]
reason: "React hooks for chat state management"
- path: "components/ThemeProvider.tsx"
must_contain: ["createContext", "useContext", "ChatThemeProvider"]
reason: "React context for chat theme/styling"
vue:
- path: "components/ChatContainer.vue"
must_contain: ["<template>", "<script setup>", "ref", "computed"]
reason: "Vue 3 Composition API chat container"
- path: "composables/useChat.ts"
must_contain: ["ref", "computed", "watch"]
reason: "Vue composable for chat state"
svelte:
- path: "components/ChatContainer.svelte"
must_contain: ["<script>", "let messages", "$:"]
reason: "Svelte chat container with reactive state"
- path: "stores/chatStore.ts"
must_contain: ["writable", "subscribe", "update"]
reason: "Svelte store for chat state management"
vanilla:
- path: "chat.js"
must_contain: ["EventSource", "addEventListener", "innerHTML"]
reason: "Vanilla JavaScript chat implementation"
- path: "index.html"
must_contain: ["<div id=\"chat\"", "<script src=\"chat.js\""]
reason: "HTML structure for vanilla implementation"
styling:
tailwind:
- path: "components/Message.tsx"
must_contain: ["className", "bg-blue-500", "dark:bg-blue-700"]
reason: "Tailwind classes for message styling"
- path: "tailwind.config.js"
must_contain: ["theme", "extend", "colors", "chat"]
reason: "Tailwind configuration with chat-specific utilities"
css_modules:
- path: "components/Message.module.css"
must_contain: [".message", ".user", ".assistant", "composes:"]
reason: "CSS Modules for scoped message styling"
- path: "components/ChatContainer.module.css"
must_contain: [".container", ".messages", ".input"]
reason: "CSS Modules for chat layout"
styled_components:
- path: "components/StyledMessage.tsx"
must_contain: ["styled.", "props.role", "${props =>"]
reason: "Styled-components with dynamic role-based styling"
- path: "styles/theme.ts"
must_contain: ["export const chatTheme", "colors", "message"]
reason: "Theme object for styled-components"
scss:
- path: "styles/chat.scss"
must_contain: [".chat-container", ".message", "&.user", "&.assistant"]
reason: "SCSS with nested selectors for chat UI"
state_management:
context:
- path: "context/ChatContext.tsx"
must_contain: ["createContext", "ChatProvider", "useChatContext"]
reason: "React Context for global chat state"
zustand:
- path: "stores/useChatStore.ts"
must_contain: ["create", "persist", "messages", "sendMessage"]
reason: "Zustand store with persistence for chat history"
redux:
- path: "store/chatSlice.ts"
must_contain: ["createSlice", "addMessage", "setStreaming"]
reason: "Redux Toolkit slice for chat state"
- path: "store/chatThunks.ts"
must_contain: ["createAsyncThunk", "streamMessage"]
reason: "Redux async actions for streaming"
pinia:
- path: "stores/chatStore.ts"
must_contain: ["defineStore", "pinia", "messages", "sendMessage"]
reason: "Pinia store for Vue chat state"
# Scaffolding files that should be created as starting points
scaffolding:
- path: "components/ChatContainer.tsx"
reason: "Initialize main chat interface container"
- path: "components/Message.tsx"
reason: "Initialize message display component with role-based styling"
- path: "components/InputArea.tsx"
reason: "Initialize user input component with keyboard shortcuts"
- path: "components/MessageList.tsx"
reason: "Initialize scrollable message list container"
- path: "app/api/chat/route.ts"
reason: "Initialize server-side streaming endpoint"
- path: "lib/chat-utils.ts"
reason: "Initialize utility functions (formatTime, sanitize, etc.)"
- path: "styles/chat.css"
reason: "Initialize basic chat UI styles with design tokens"
- path: "package.json"
reason: "Initialize with required AI chat dependencies"
- path: "tsconfig.json"
reason: "TypeScript configuration for type safety"
- path: ".env.local"
reason: "Environment variables for API keys (OPENAI_API_KEY, etc.)"
- path: ".gitignore"
reason: "Ignore node_modules, .env.local, build artifacts"
- path: "README.md"
reason: "Document AI chat implementation, features, and usage"
# Metadata
metadata:
primary_blueprints: ["rag-pipeline", "ai-ml", "dashboard"]
contributes_to:
- "AI chat interface (ChatGPT-style conversational UI)"
- "Streaming responses (token-by-token rendering with markdown)"
- "Message components (user, AI, system bubbles)"
- "Context management (token limits, conversation summarization)"
- "Multi-modal input (text, images, files, voice)"
- "Feedback mechanisms (thumbs up/down, regenerate, copy)"
- "Tool usage visualization (function calling display)"
- "AI-specific error handling (refusals, rate limits, hallucinations)"
- "Accessibility (screen readers, keyboard navigation)"
- "Performance optimization (memoization, virtual scrolling)"
common_patterns:
- "Vercel AI SDK (useChat hook for streaming)"
- "@vercel/streamdown (incomplete markdown rendering)"
- "Server-Sent Events (SSE) for real-time streaming"
- "Auto-scroll with user activity detection"
- "DOMPurify for XSS protection on AI outputs"
- "Token indicator with user-friendly metaphors"
- "Stop generation button during streaming"
- "Regenerate/continue controls after completion"
- "Message feedback (thumbs up/down for RLHF)"
- "Copy to clipboard for message sharing"
- "Multi-modal file upload with preview"
- "Voice input via Web Speech API"
- "ARIA live regions for screen reader support"
- "Memoized message rendering for performance"
- "Debounced streaming updates (50ms)"
- "Virtual scrolling for long conversations"
integration_points:
design_tokens: "Uses design tokens for theming (message colors, spacing, shadows)"
forms: "Input area uses form validation patterns"
feedback: "Error displays use feedback component patterns"
data_viz: "Tool usage can visualize data with charts"
dashboards: "Can be embedded as dashboard widget"
navigation: "Conversation history navigation patterns"
typical_directory_structure: |
project/
├── components/ # React chat components
│ ├── ChatContainer.tsx # Main chat interface
│ ├── MessageList.tsx # Scrollable message container
│ ├── Message.tsx # Individual message bubble
│ ├── InputArea.tsx # User input with attachments
│ ├── MessageActions.tsx # Feedback controls (thumbs, copy)
│ ├── ResponseControls.tsx # Stop, regenerate, continue, edit
│ ├── TokenIndicator.tsx # Context limit visualization
│ ├── MultiModalInput.tsx # Image/file/voice input
│ ├── ToolUsageDisplay.tsx # Function calling visualization
│ ├── ErrorDisplay.tsx # AI-specific error handling
│ ├── ConversationBranching.tsx # Thread navigation
│ └── ChatAccessibility.tsx # ARIA live regions
├── hooks/ # Custom React hooks
│ ├── useChat.ts # Chat state management
│ ├── useAutoScroll.ts # Smart scroll behavior
│ ├── useVirtualScroll.ts # Long conversation optimization
│ └── useVoiceInput.ts # Web Speech API integration
├── lib/ # Utility functions
│ ├── chat-utils.ts # Formatting, sanitization
│ ├── streaming-optimizations.ts # Memoization, debouncing
│ ├── context-management.ts # Token counting, summarization
│ ├── auto-scroll.ts # Scroll heuristics
│ ├── sanitize.ts # DOMPurify configuration
│ └── voice-integration.ts # Voice input/output
├── app/api/chat/ # API routes
│ ├── route.ts # Streaming endpoint
│ └── feedback/route.ts # Feedback collection
├── styles/ # CSS/styling
│ ├── chat.css # Chat UI styles
│ ├── chat.module.css # CSS Modules
│ └── chat.scss # SCSS styles
├── scripts/ # Token-free utilities
│ ├── parse_stream.js # Parse incomplete markdown
│ ├── calculate_tokens.py # Token estimation
│ └── format_messages.js # Export conversation history
├── examples/ # Usage examples
│ ├── basic-chat.tsx # Minimal ChatGPT-style interface
│ ├── streaming-chat.tsx # Advanced streaming with memoization
│ ├── multimodal-chat.tsx # Image and file uploads
│ ├── code-assistant.tsx # IDE-style code copilot
│ └── tool-calling-chat.tsx # Function calling visualization
├── assets/ # Static resources
│ ├── system-prompts.json # Curated prompts for different use cases
│ ├── message-templates.json # Pre-built message components
│ ├── error-messages.json # User-friendly error messages
│ └── themes.json # Light, dark, high-contrast themes
├── references/ # Documentation
│ ├── streaming-ux.md # Streaming patterns and auto-scroll
│ ├── context-management.md # Token limits and strategies
│ ├── multi-modal.md # Image, file, voice handling
│ ├── feedback-loops.md # RLHF patterns
│ ├── error-handling.md # AI-specific error scenarios
│ ├── tool-usage.md # Function calling visualization
│ ├── accessibility.md # Screen reader and keyboard support
│ ├── library-guide.md # Detailed library documentation
│ └── performance-optimization.md # Streaming performance
├── tests/ # Unit tests
│ ├── chat.test.tsx # Component tests
│ ├── hooks.test.ts # Hook tests
│ └── utils.test.ts # Utility function tests
├── package.json # Dependencies (ai, streamdown, etc.)
├── tsconfig.json # TypeScript configuration
├── .env.local # API keys
└── README.md # Documentation
tools_required:
- name: "Vercel AI SDK"
version: "^3.0.0"
purpose: "Streaming AI chat with useChat hook"
install: "npm install ai @ai-sdk/react @ai-sdk/openai"
- name: "@vercel/streamdown"
version: "^1.0.0"
purpose: "Render incomplete markdown during streaming"
install: "npm install @vercel/streamdown"
- name: "react-syntax-highlighter"
version: "^15.0.0"
purpose: "Syntax highlighting for code blocks in messages"
install: "npm install react-syntax-highlighter @types/react-syntax-highlighter"
- name: "dompurify"
version: "^3.0.0"
purpose: "XSS protection for AI-generated HTML"
install: "npm install dompurify @types/dompurify"
- name: "react-window"
version: "^1.8.0"
purpose: "Virtual scrolling for long conversations"
install: "npm install react-window @types/react-window"
- name: "tiktoken"
version: "^1.0.0"
purpose: "Token counting for context management"
install: "npm install tiktoken"
validation_checks:
- "useChat hook from ai/react imported and used"
- "@vercel/streamdown used for AI message rendering"
- "DOMPurify sanitizes AI-generated content"
- "Auto-scroll respects user scrolling (not always scrolling)"
- "Stop button visible during streaming"
- "Regenerate/continue controls visible after completion"
- "Token indicator shows user-friendly context status"
- "Message feedback (thumbs) sends data to backend"
- "Copy to clipboard works on all messages"
- "Keyboard shortcuts (Enter to send, Shift+Enter for newline)"
- "ARIA live regions announce new messages to screen readers"
- "Messages memoized to prevent re-renders during streaming"
- "Multi-modal input validates file types and sizes"
- "Error handling differentiates AI-specific errors (refusals, rate limits)"
- "Voice input has visual feedback (recording indicator)"
anti_patterns:
- name: "No streaming support"
avoid: "Waiting for full response before displaying"
use: "Token-by-token streaming with SSE or useChat hook"
- name: "Always auto-scrolling"
avoid: "Forcing scroll even when user reading previous messages"
use: "Smart auto-scroll that detects user activity"
- name: "Unsanitized AI output"
avoid: "Directly rendering AI-generated HTML/markdown"
use: "DOMPurify.sanitize() before rendering"
- name: "No stop generation button"
avoid: "User cannot interrupt long/unwanted responses"
use: "Stop button visible during streaming with immediate effect"
- name: "Technical token counts"
avoid: "Showing raw numbers like '3,847 / 8,000 tokens'"
use: "User-friendly metaphors like '~15 messages remaining'"
- name: "No feedback mechanism"
avoid: "No way for users to rate responses"
use: "Thumbs up/down, regenerate, and copy controls"
- name: "Missing error context"
avoid: "Generic 'Error occurred' messages"
use: "Specific handling for refusals, rate limits, context overflows"
- name: "Blocking message rendering"
avoid: "Re-rendering all messages on every token"
use: "React.memo() for messages, only update streaming message"
- name: "No keyboard shortcuts"
avoid: "Only mouse/touch interaction"
use: "Enter to send, Shift+Enter for newline, Escape to cancel"
- name: "No accessibility support"
avoid: "Screen readers unaware of new messages"
use: "ARIA live regions (role='log', aria-live='polite')"
- name: "Ignoring multi-modal capabilities"
avoid: "Text-only interface when AI supports images"
use: "File/image upload with preview and voice input"
- name: "No context management"
avoid: "Letting conversation exceed token limits unexpectedly"
use: "Token indicator with summarization or pruning strategy"
AI Chat Accessibility Guide
WCAG 2.1 AA compliance for AI chat interfaces with screen reader support, keyboard navigation, and ARIA patterns.
Table of Contents
- Core Requirements
- 1. Semantic HTML and ARIA
- 2. Keyboard Navigation
- 3. Focus Management
- Screen Reader Announcements
- Status Announcements
- Progress Updates
- Visual Accessibility
- Color Contrast
- Focus Indicators
- Text Sizing
- Alternative Text for Images
- Loading States
- Error Messages
- Code Block Accessibility
- Mobile Accessibility
- Touch Targets
- Reduced Motion
- Testing Checklist
- Resources
Core Requirements
1. Semantic HTML and ARIA
<div role="log" aria-live="polite" aria-atomic="false" aria-relevant="additions">
{messages.map((msg) => (
<div
key={msg.id}
role="article"
aria-label={`Message from ${msg.role}`}
>
{msg.content}
</div>
))}
</div>Key attributes:
role="log"- Messages appear chronologicallyaria-live="polite"- Screen reader announces new messagesaria-atomic="false"- Only announce new additionsrole="article"- Each message is a discrete unit
2. Keyboard Navigation
function ChatInput() {
const handleKeyDown = (e: KeyboardEvent) => {
// Send with Enter
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
// Newline with Shift+Enter
if (e.key === 'Enter' && e.shiftKey) {
// Allow default behavior (newline)
}
// Stop generation with Escape
if (e.key === 'Escape' && isStreaming) {
stopGeneration();
}
};
return (
<textarea
onKeyDown={handleKeyDown}
aria-label="Chat message input"
placeholder="Type your message..."
/>
);
}Keyboard shortcuts:
Enter- Send messageShift+Enter- New lineEscape- Stop generationCmd/Ctrl+K- Focus input↑/↓- Navigate message history
3. Focus Management
import { useEffect, useRef } from 'react';
function ChatInterface() {
const inputRef = useRef<HTMLTextAreaElement>(null);
// Focus input after send
useEffect(() => {
if (!isStreaming) {
inputRef.current?.focus();
}
}, [isStreaming]);
// Trap focus during modal
const handleTabKey = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
// Keep focus within chat interface
}
};
return <textarea ref={inputRef} onKeyDown={handleTabKey} />;
}Screen Reader Announcements
Status Announcements
import { useAnnouncer } from '@react-aria/live-announcer';
function ChatInterface() {
const { announce } = useAnnouncer();
useEffect(() => {
if (isStreaming) {
announce('AI is responding', 'polite');
}
if (isComplete) {
announce('Response complete', 'polite');
}
if (error) {
announce(`Error: ${error.message}`, 'assertive');
}
}, [isStreaming, isComplete, error]);
}Progress Updates
// Announce progress for long responses
useEffect(() => {
if (tokenCount > 0 && tokenCount % 100 === 0) {
announce(`${tokenCount} tokens generated`, 'polite');
}
}, [tokenCount]);Visual Accessibility
Color Contrast
// WCAG AA minimum: 4.5:1 for normal text, 3:1 for large text
const colors = {
userMessage: {
background: '#3b82f6', // Blue
text: '#ffffff', // White (contrast: 8.6:1 ✓)
},
aiMessage: {
background: '#f3f4f6', // Light gray
text: '#1f2937', // Dark gray (contrast: 12.6:1 ✓)
},
};Focus Indicators
/* Visible focus outline */
button:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
textarea:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 0;
}Text Sizing
// Allow user to adjust text size
const [fontSize, setFontSize] = useState(16);
return (
<div style={{ fontSize: `${fontSize}px` }}>
{messages.map((msg) => <Message {...msg} />)}
</div>
);Alternative Text for Images
// Multi-modal chat with image input
function ImageMessage({ image, description }: Props) {
return (
<div>
<img
src={image.url}
alt={description || 'User uploaded image'}
aria-describedby={`img-desc-${image.id}`}
/>
<p id={`img-desc-${image.id}`} className="sr-only">
{description || 'Image uploaded by user for AI analysis'}
</p>
</div>
);
}Loading States
function ChatMessage({ message, isStreaming }: Props) {
if (!message.content && isStreaming) {
return (
<div
role="status"
aria-label="AI is generating response"
aria-live="polite"
>
<div className="flex gap-1">
<span className="animate-pulse">●</span>
<span className="animate-pulse animation-delay-75">●</span>
<span className="animate-pulse animation-delay-150">●</span>
</div>
<span className="sr-only">AI is thinking...</span>
</div>
);
}
return <div>{message.content}</div>;
}Error Messages
function ErrorMessage({ error }: { error: Error }) {
return (
<div
role="alert"
aria-live="assertive"
className="error-banner"
>
<span aria-hidden="true">⚠️</span>
<span>{error.message}</span>
<button aria-label="Retry message">Retry</button>
<button aria-label="Dismiss error">Dismiss</button>
</div>
);
}Code Block Accessibility
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
function CodeBlock({ code, language }: Props) {
const [copied, setCopied] = useState(false);
return (
<div role="region" aria-label={`Code block in ${language}`}>
<div className="code-header">
<span>{language}</span>
<button
onClick={() => {
navigator.clipboard.writeText(code);
setCopied(true);
}}
aria-label={copied ? 'Code copied' : 'Copy code to clipboard'}
>
{copied ? '✓ Copied' : '📋 Copy'}
</button>
</div>
<SyntaxHighlighter
language={language}
customStyle={{ fontSize: '14px' }}
wrapLongLines={true}
>
{code}
</SyntaxHighlighter>
</div>
);
}Mobile Accessibility
Touch Targets
// Minimum 44x44px touch targets (WCAG 2.5.5)
const buttonStyles = {
minHeight: '44px',
minWidth: '44px',
padding: '12px 16px',
};
<button style={buttonStyles}>Send</button>Reduced Motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
<div
className={prefersReducedMotion ? '' : 'animate-slide-up'}
>
{message.content}
</div>Testing Checklist
- [ ] Screen reader announces new messages
- [ ] All controls keyboard-accessible
- [ ] Focus visible on all interactive elements
- [ ] Color contrast meets WCAG AA (4.5:1)
- [ ] Touch targets ≥44x44px
- [ ] Alt text for all images
- [ ] Errors announced with
role="alert" - [ ] Loading states announced
- [ ] Stop button accessible during streaming
- [ ] Works with reduced motion preference
Resources
- WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/
- ARIA Authoring Practices: https://www.w3.org/WAI/ARIA/apg/
- React Aria: https://react-spectrum.adobe.com/react-aria/
Streaming UX Patterns for AI Chat
Best practices for user experience when streaming LLM responses token-by-token.
Table of Contents
- Core Principles
- Visual Patterns
- Loading States
- Progressive Token Display
- Markdown Rendering While Streaming
- Interaction Patterns
- Stop Generation Button
- Regenerate Response
- Typing Indicator (User)
- Performance Optimizations
- Throttle Re-renders
- Virtual Scrolling for Long Responses
- Debounced Auto-scroll
- Error Handling
- Network Error Display
- Partial Response Recovery
- Accessibility
- Screen Reader Announcements
- Keyboard Shortcuts
- Best Practices
- Anti-Patterns
Core Principles
1. Show immediate feedback - Don't wait for first token 2. Stream progressively - Append tokens as they arrive 3. Indicate completion - Clear visual cue when done 4. Handle errors gracefully - Don't leave users hanging 5. Enable interruption - Allow users to stop generation
Visual Patterns
Loading States
Before first token arrives:
{isLoading && !response && (
<div className="flex items-center gap-2">
<div className="animate-pulse">●</div>
<span className="text-gray-500">Thinking...</span>
</div>
)}While streaming:
{isStreaming && (
<div className="inline-block animate-pulse ml-1">▊</div>
)}Completion indicator:
{!isStreaming && response && (
<div className="text-xs text-gray-400 mt-1">
✓ Complete • {tokenCount} tokens
</div>
)}Progressive Token Display
import { useState } from 'react';
function StreamingMessage({ message }: { message: string }) {
return (
<div className="prose">
{message}
<span className="inline-block w-1 h-4 bg-blue-500 animate-pulse ml-1" />
</div>
);
}Markdown Rendering While Streaming
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
function StreamingMarkdown({ content }: { content: string }) {
return (
<ReactMarkdown
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{content}
</ReactMarkdown>
);
}Interaction Patterns
Stop Generation Button
function ChatInterface() {
const [isStreaming, setIsStreaming] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const stopGeneration = () => {
abortControllerRef.current?.abort();
setIsStreaming(false);
};
const sendMessage = async (prompt: string) => {
abortControllerRef.current = new AbortController();
setIsStreaming(true);
try {
const response = await fetch('/api/chat', {
method: 'POST',
signal: abortControllerRef.current.signal,
body: JSON.stringify({ prompt }),
});
const reader = response.body.getReader();
// ... streaming logic
} catch (error) {
if (error.name === 'AbortError') {
console.log('Generation stopped by user');
}
} finally {
setIsStreaming(false);
}
};
return (
<div>
{isStreaming && (
<button onClick={stopGeneration} className="stop-button">
⬛ Stop Generating
</button>
)}
</div>
);
}Regenerate Response
function MessageActions({ messageId, onRegenerate }: Props) {
return (
<div className="flex gap-2 mt-2 opacity-0 group-hover:opacity-100 transition">
<button onClick={() => onRegenerate(messageId)} className="text-sm">
🔄 Regenerate
</button>
<button className="text-sm">📋 Copy</button>
<button className="text-sm">👍 Good</button>
<button className="text-sm">👎 Bad</button>
</div>
);
}Typing Indicator (User)
function TypingIndicator({ isTyping }: { isTyping: boolean }) {
if (!isTyping) return null;
return (
<div className="flex items-center gap-2 text-gray-500 text-sm">
<div className="flex gap-1">
<span className="animate-bounce" style={{ animationDelay: '0ms' }}>●</span>
<span className="animate-bounce" style={{ animationDelay: '150ms' }}>●</span>
<span className="animate-bounce" style={{ animationDelay: '300ms' }}>●</span>
</div>
AI is typing...
</div>
);
}Performance Optimizations
Throttle Re-renders
import { useMemo } from 'react';
function StreamingChat() {
const [tokens, setTokens] = useState<string[]>([]);
// Only update every 50ms (not every token)
const displayText = useMemo(() => {
return tokens.join('');
}, [tokens]);
return <div>{displayText}</div>;
}Virtual Scrolling for Long Responses
import { useVirtualizer } from '@tanstack/react-virtual';
function MessageList({ messages }: { messages: Message[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((item) => (
<div
key={item.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${item.start}px)`,
}}
>
<MessageComponent message={messages[item.index]} />
</div>
))}
</div>
</div>
);
}Debounced Auto-scroll
import { useEffect, useRef } from 'react';
function ChatContainer({ messages, isStreaming }: Props) {
const endRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isStreaming) {
endRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [messages, isStreaming]);
return (
<div className="messages">
{messages.map((msg) => <Message key={msg.id} {...msg} />)}
<div ref={endRef} />
</div>
);
}Error Handling
Network Error Display
{error && (
<div className="error-message">
<span>⚠️ Connection error. </span>
<button onClick={retry}>Try again</button>
</div>
)}Partial Response Recovery
async function streamWithRecovery(prompt: string) {
let partialResponse = '';
try {
for await (const token of streamTokens(prompt)) {
partialResponse += token;
setResponse(partialResponse);
}
} catch (error) {
// Keep partial response, allow retry
console.error('Stream interrupted:', error);
setError('Connection lost. Partial response saved.');
// partialResponse is still available
}
}Accessibility
Screen Reader Announcements
import { useAnnouncer } from '@react-aria/live-announcer';
function StreamingChat() {
const { announce } = useAnnouncer();
useEffect(() => {
if (isComplete) {
announce('Response complete', 'polite');
}
}, [isComplete]);
return <div role="log" aria-live="polite" aria-atomic="false">
{response}
</div>;
}Keyboard Shortcuts
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isStreaming) {
stopGeneration();
}
if (e.metaKey && e.key === 'k') {
e.preventDefault();
focusInput();
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, [isStreaming]);Best Practices
1. Show loading state immediately - Don't wait for first token 2. Use cursor/pulse animation - Indicates active streaming 3. Auto-scroll to bottom - Keep latest content visible 4. Allow stopping - ESC key or stop button 5. Preserve partial responses - On error, don't lose what's streamed 6. Throttle updates - Update UI every 50ms, not every token 7. Visual completion - Clear indicator when done 8. Enable regeneration - Let users retry bad responses 9. Format incrementally - Render markdown as it streams 10. Provide feedback actions - Good/bad buttons, copy, share
Anti-Patterns
❌ Don't:
- Wait for complete response before displaying
- Re-render entire component on every token
- Scroll to top on each update
- Block UI during streaming
- Hide partial responses on error
- Use polling instead of streaming
✅ Do:
- Stream progressively with SSE/WebSocket
- Update UI efficiently (throttle, memo)
- Auto-scroll smoothly to bottom
- Allow interaction during streaming
- Preserve partial content
- Use proper streaming protocols
Related skills
FAQ
What does building-ai-chat handle?
Streaming text, token limits, regeneration, feedback loops, tool usage visualization, and AI-specific error patterns.
What library does it use for streaming markdown?
It uses Streamdown (@vercel/streamdown) which handles incomplete markdown during streaming.