Claude-skill-registry chatbot-widget-creator
Creates a battle-tested ChatGPT-style chatbot widget that solves real-world production issues. Features infinite re-render protection, text selection "Ask AI", RAG backend integration, streaming SSE, and comprehensive performance monitoring.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/chatbot-widget-creator" ~/.claude/skills/majiayu000-claude-skill-registry-chatbot-widget-creator && rm -rf "$T"
manifest:
skills/data/chatbot-widget-creator/SKILL.mdsource content
Chatbot Widget Creator Skill
Purpose
Creates a production-ready ChatGPT-style chatbot widget with advanced features:
- Custom Portrait Layout: Compact 380px x 700px styling matching ChatGPT aesthetics
- Infinite re-render protection using useReducer and split context pattern
- Text selection "Ask AI" functionality with smart tooltips
- Streaming responses with Server-Sent Events (SSE)
- RAG backend integration ready for FastAPI/Qdrant setup
- Performance monitoring and debugging utilities
- Modern glassmorphic UI with ChatGPT-style interface
Key Improvements Based on Real Implementation
1. Refined UI/UX
- Compact Dimensions: 380px width x 700px height for optimal sidebar integration
- Clean Message UI: Removed timestamps, read receipts, and file uploads for a cleaner reading experience
- Themed Integration: Dynamically uses
to match host site brandingvar(--ifm-color-primary) - Bottom Alignment: Message bubbles and avatars are bottom-aligned for a modern chat look
- Minimalist Indicators: Simplified 3-dot pulsing animation for thinking state
2. State Management Architecture
- useReducer pattern instead of multiple useState hooks
- Split context (StateContext + ActionsContext) to prevent unnecessary re-renders
- Stable callbacks with proper dependencies to avoid circular references
- AbortController for proper cleanup of streaming connections
3. Performance Optimizations
- React.memo wrapping for expensive components like
MessageBubble - useMemo for computed values and complex operations
- useCallback with stable dependencies
- Render counter utilities for debugging
- Virtualization support for long conversations (50+ messages)
4. Text Selection Feature
- useTextSelection hook for detecting text selections
- Smart positioning tooltip with edge detection
- Context-aware prompts when asking about selected text
- Length validation with truncation warnings
Prerequisites
-
Backend Requirements:
- FastAPI or similar with SSE support
- Endpoint:
returning Server-Sent EventsPOST /api/chat - Request format:
{ "question": string, "stream": boolean } - Response format: SSE with
{ "type": "chunk", "content": string }
-
Frontend Dependencies:
npm install react framer-motion react-markdown react-syntax-highlighter remark-gfm lucide-react clsx tailwind-merge # Note: No ChatKit dependency - this is a custom implementation
Quick Start
1. Create the Widget Structure
# Create main directory structure mkdir -p src/components/ChatWidget/{components,hooks,contexts,utils,styles} # Copy all template files cp -r .claude/skills/chatbot-widget-creator/templates/* src/components/ChatWidget/
2. Backend API Requirements
Your backend must implement:
@app.post("/api/chat") async def chat_endpoint(request: ChatRequest): """Chat endpoint with Server-Sent Events streaming.""" # Request format { "question": "What is physical AI?", "stream": true, "context": { # Optional "selectedText": "...", "source": "User selection" } } # Response format (SSE) data: {"type": "start", "session_id": "..."} data: {"type": "chunk", "content": "Physical AI refers to..."} data: {"type": "chunk", "content": "embodied AI systems..."} data: {"type": "done", "session_id": "..."}
3. Integration
Add to your site root (e.g.,
src/theme/Root.tsx):
import React from 'react'; import AnimatedChatWidget from '../components/ChatWidget/components/AnimatedChatWidget'; export default function Root({ children }) { const getChatEndpoint = () => { const hostname = window.location.hostname; if (hostname === 'localhost') { return 'http://localhost:7860/api/chat'; } return 'https://your-domain.com/api/chat'; }; return ( <> {children} <AnimatedChatWidget apiUrl={getChatEndpoint()} maxTextSelectionLength={2000} fallbackTextLength={5000} /> </> ); }
Architecture Details
State Management (Critical for Performance)
// Consolidated state to prevent fragmentation interface ChatState { messages: ChatMessage[]; isOpen: boolean; isThinking: boolean; currentStreamingId?: string; error: Error | null; renderCount: number; } // Split context pattern const ChatStateContext = createContext<{ state: ChatState }>(); const ChatActionsContext = createContext<{ actions: ChatActions }>(); // Components only subscribe to what they need const messages = useChatSelector(s => s.messages); // Re-renders on messages change const actions = useChatActions(); // Never re-renders
Key Anti-Patterns Avoided
-
❌ Multiple useState hooks:
// Bad - causes context fragmentation const [messages, setMessages] = useState([]); const [isOpen, setIsOpen] = useState(false); -
✅ Consolidated useReducer:
// Good - single state source const [state, dispatch] = useReducer(chatReducer, initialState); -
❌ Circular dependencies:
// Bad - callback depends on state that changes const handleChunk = useCallback((chunk) => { if (session.currentStreamingId) { // Dependency on state updateMessage(session.currentStreamingId, chunk); } }, [session.currentStreamingId]); // Infinite re-render! -
✅ Stable references:
// Good - no circular dependencies const streamingIdRef = useRef<string>(); const handleChunk = useCallback((chunk) => { if (streamingIdRef.current) { dispatch(updateStreamingAction(streamingIdRef.current, chunk)); } }, [dispatch]); // Stable dependency array
Streaming Response Handling
// Proper SSE parsing const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data !== '[DONE]') { const parsed = JSON.parse(data); if (parsed.type === 'chunk' && parsed.content) { handleChunk(parsed.content); } else if (parsed.type === 'done') { handleComplete(); } } } }
Customization Guide
Theming
Edit
src/components/ChatWidget/styles/ChatWidget.module.css:
/* Primary colors */ .widget { background: #171717; /* Dark mode background */ width: 380px; height: 700px; } /* Message bubbles */ .userMessageBubble { background: var(--ifm-color-primary); /* Theme integration */ color: white; } .aiMessageBubble { background: #21262d; border: 1px solid #30363d; }
Component Reference
Core Components
- AnimatedChatWidget: Main entry point with animations
- ChatInterface: ChatGPT-style UI container
- MessageBubble: Individual message display (React.memo optimized)
- InputArea: Message input (No file upload)
- SelectionTooltip: Text selection "Ask AI" tooltip
- ThinkingIndicator: Minimalist 3-dot pulsing animation
Hooks
- useChatSession: Access chat state and actions
- useStreamingResponse: Handle SSE connections
- useTextSelection: Detect and handle text selection
- useErrorHandler: Centralized error management
Utilities
- chatReducer: State transitions
- api.ts: API request/response formatting
- animations.ts: Framer Motion configs
Troubleshooting
Common Issues and Solutions
-
Infinite Re-renders
- Check for circular dependencies in useCallback
- Ensure split context pattern is properly implemented
- Use React DevTools Profiler to identify causes
-
Memory Leaks
- Ensure AbortController cleanup on unmount
- Check for unclosed SSE connections
- Monitor with
window.performance.memory
-
SSE Not Working
- Verify CORS headers include
text/event-stream - Check that responses use correct
formatdata: {} - Ensure
is setCache-Control: no-cache - Crucial: Ensure your API request includes
in the bodystream: true - Extract content correctly:
not the raw stringhandleChunk(JSON.parse(data).content)
- Verify CORS headers include
-
Text Selection Issues
- Verify
is enableduseTextSelection - Check for CSS
conflictsuser-select: none - Ensure z-index is high enough for tooltip
- Verify
-
Markdown Rendering & Layout Issues
- List Numbers on Separate Lines:
- Set
onlist-style-position: inside
/ulol - Reset
margins in lists:pli > p { margin: 0; display: inline; }
- Set
- Invisible Text in Light Mode:
- Explicitly define text colors for
, headings, and lists in light mode media queries or classes.messageRenderer - Ensure
background is transparent or correctly colored.aiMessageBubble
- Explicitly define text colors for
- Excessive Spacing:
- Reduce margins on
,p
,ul
(e.g.,ol
)0.25rem - Remove margins from first/last children:
.messageRenderer > *:first-child { margin-top: 0; } - Use
if global site styles (like Docusaurus) override your chat styles!important
- Reduce margins on
- Avatar Alignment:
- Use CSS Grid for message layout to align avatar with the bottom of the text
- Example:
.aiMessage { display: grid; align-items: end; }
- List Numbers on Separate Lines:
Production Checklist
- Remove console.log statements in production
- Add proper error tracking (Sentry, etc.)
- Implement rate limiting on backend
- Add CORS configuration
- Test on slow networks
- Verify memory leak prevention
- Test with long conversations (100+ messages)
Result
A production-ready chat widget that:
- Matches the ChatGPT visual aesthetic
- Never crashes from infinite re-renders
- Provides smooth text selection interactions
- Streams responses efficiently
- Maintains performance over time
- Handles all edge cases gracefully