LoadingSpinner Component
Overview
The LoadingSpinner component provides an enhanced loading indicator with modern design and intelligent thinking content display. It’s used throughout the application to show streaming status and provide real-time feedback about AI processing.
Features
Visual Design
- Pixelated Spinner: Aitana-inspired 2x2 grid of squares with staggered pulse animations
- Color Scheme: Uses Aitana brand colors (
#FF9966,#FFB088) - Center Pulse: Animated center circle with ping effect
- Responsive Layout: Flexbox layout that adapts to container width
Thinking Content Display
- Real-time Updates: Shows latest AI thinking content during processing
- Content Cleaning: Automatically removes HTML tags and markdown formatting
- Smart Truncation: Uses scrolling animation for long content (>80 characters)
- Latest Content Priority: Displays most recent substantive thinking, filtering out generic status messages
Performance Features
- Conditional Rendering: Only renders when
isStreamingis true - Memoized Processing: Uses
React.useMemofor thinking content processing - Animation Reset: Resets scroll animation when content changes significantly
Component Interface
Props
interface LoadingSpinnerProps {
isStreaming: boolean; // Controls component visibility
maxSnippetLength?: number; // Maximum length for content display (default: 120)
thinkingContent?: string; // AI thinking content to display
className?: ClassNameValue; // Additional CSS classes
}
Usage Examples
Basic Usage
import { LoadingSpinner } from '@/components/LoadingSpinner'
// Simple loading indicator
<LoadingSpinner isStreaming={true} />
With Thinking Content
// Display AI thinking content
<LoadingSpinner
isStreaming={true}
thinkingContent="I'm analyzing the user's question and considering the best approach to provide a helpful response."
/>
With Custom Styling
// Custom styling and configuration
<LoadingSpinner
isStreaming={isProcessing}
thinkingContent={currentThinking}
maxSnippetLength={150}
className="my-4 bg-gray-50 rounded-lg"
/>
Integration Points
Chat Interface
File: src/components/ChatInterface.tsx
The LoadingSpinner is integrated into chat streaming to show AI processing status:
{isStreaming && (
<LoadingSpinner
isStreaming={isStreaming}
thinkingContent={currentThinking}
className="border-t border-gray-100"
/>
)}
Message Streaming
Context: MessageStreamingContext
Works with streaming context to receive real-time thinking updates:
const { isStreaming, thinkingContent } = useMessageStreaming()
return (
<LoadingSpinner
isStreaming={isStreaming}
thinkingContent={thinkingContent}
/>
)
Content Processing Algorithm
Thinking Content Extraction
The component implements sophisticated content cleaning:
- Latest Content Selection:
// Work backwards from newest thinking content for (let i = thinkingLines.length - 1; i >= 0; i--) { const line = thinkingLines[i].trim(); // Skip very short or generic lines if (line.length < 10) continue; // Skip generic status messages if (line.match(/^(thinking|processing|analyzing)\.{0,3}$/i)) { continue; } latestThinking = line; break; } - HTML and Markdown Cleanup:
const cleanContent = latestThinking .replace(/<[^>]*>/g, '') // Remove HTML tags .replace(/```[\s\S]*?```/g, '[code]') // Replace code blocks .replace(/`([^`]+)`/g, '$1') // Remove inline code ticks .replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold markers .replace(/#{1,6}\s+/g, '') // Remove heading markers .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove markdown links .replace(/\s+/g, ' ') // Normalize whitespace .trim(); - Animation Triggering:
// Reset animation when content changes significantly useEffect(() => { if (thinkingSnippet !== previousSnippetRef.current && thinkingSnippet.length > 80) { setAnimationKey(prev => prev + 1); previousSnippetRef.current = thinkingSnippet; } }, [thinkingSnippet]);
Styling and Animation
CSS Classes and Animations
Container Layout:
<div className="flex items-center justify-start py-3 px-4">
<div className="flex items-center gap-3 max-w-4xl w-full">
Spinner Animation:
{/* Four corners with staggered pulse */}
<div className="absolute w-2 h-2 bg-[#FF9966] rounded-sm animate-pulse"
style={{ animationDelay: '0s', animationDuration: '1.5s' }}>
</div>
{/* Center ping animation */}
<div className="absolute ... animate-ping"
style={{ animationDuration: '2s' }}>
</div>
Scrolling Animation:
<div style={{ animation: 'marquee 8s linear infinite' }}>
{thinkingSnippet}
</div>
Custom CSS (Required)
Add to your global CSS for marquee animation:
@keyframes marquee {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
Testing
Test Coverage
File: src/components/__tests__/LoadingSpinner.test.tsx
The component has comprehensive test coverage including:
Rendering Tests
it('should render when isStreaming is true', () => {
const { container } = render(<LoadingSpinner isStreaming={true} />)
expect(container.firstElementChild).not.toBeNull()
})
it('should not render when isStreaming is false', () => {
const { container } = render(<LoadingSpinner isStreaming={false} />)
expect(container.firstElementChild).toBeNull()
})
Content Processing Tests
it('should clean thinking content by removing markdown syntax', () => {
const markdownContent = '```javascript\nconst x = 1;\n```\n**Bold text**'
render(<LoadingSpinner isStreaming={true} thinkingContent={markdownContent} />)
expect(screen.queryByText(/```/)).not.toBeInTheDocument()
expect(screen.queryByText(/\*\*/)).not.toBeInTheDocument()
})
Performance Tests
it('should handle very long content without breaking', () => {
const veryLongContent = 'A'.repeat(500)
const { container } = render(<LoadingSpinner isStreaming={true} thinkingContent={veryLongContent} />)
expect(container.firstElementChild).toBeInTheDocument()
})
Running Tests
# Run LoadingSpinner tests specifically
npm test src/components/__tests__/LoadingSpinner.test.tsx
# Run tests with coverage
npm run test:coverage -- LoadingSpinner
Performance Considerations
Optimization Strategies
-
Conditional Rendering: Component returns
nullwhen not streaming to avoid unnecessary DOM updates -
Memoized Processing: Thinking content processing is memoized to prevent recalculation on every render
-
Efficient Animation: Uses CSS animations and
transformproperties for smooth performance -
Content Filtering: Filters out low-value thinking content to reduce visual noise
Memory Management
- Uses
useRefto track previous content without triggering re-renders - Cleans up animations automatically when component unmounts
- Processes only the latest thinking content, not entire history
Accessibility
Screen Reader Support
{/* Descriptive text for screen readers */}
<span className="text-gray-400 font-normal">Thinking:</span>
{/* Fallback content when no thinking available */}
<span className="text-gray-400 font-normal">Processing</span>
Visual Accessibility
- High contrast colors for spinner elements
- Smooth animations that respect user motion preferences
- Clear visual hierarchy with proper spacing
Future Enhancements
Planned Improvements
- Motion Preferences: Add support for
prefers-reduced-motionCSS media query - Customizable Colors: Allow theme-based color customization
- Progress Indicators: Add optional progress percentage display
- Sound Feedback: Optional audio cues for accessibility
- Animation Variants: Multiple animation styles (dots, bars, pulse patterns)
Integration Opportunities
- Tool Execution Status: Show specific tool being executed
- Model Selection: Indicate which AI model is processing
- Queue Position: Show position in processing queue
- Estimated Time: Display estimated completion time
Troubleshooting
Common Issues
Animation Not Working
Problem: Scrolling animation not appearing for long content
Solution: Ensure global CSS includes marquee keyframes:
@keyframes marquee {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
Content Not Updating
Problem: Thinking content not refreshing during streaming
Check:
- Verify
isStreamingprop is being updated - Ensure
thinkingContentprop receives new values - Check component re-rendering with React Developer Tools
Styling Issues
Problem: Spinner not visible or misaligned
Solutions:
- Verify Tailwind CSS classes are available
- Check parent container has sufficient space
- Ensure brand colors are defined in Tailwind config
Debug Mode
Add temporary logging for debugging:
// Debug thinking content processing
console.log('Raw thinking:', thinkingContent)
console.log('Processed snippet:', thinkingSnippet)
console.log('Animation key:', animationKey)
Dependencies
Required Packages
react(^18.0.0)tailwind-merge- For className mergingtailwindcss- For styling
Peer Dependencies
@types/react(for TypeScript support)
Internal Dependencies
- Tailwind configuration with Aitana brand colors
- Global CSS with marquee animation keyframes