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 isStreaming is true
  • Memoized Processing: Uses React.useMemo for 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:

  1. 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;
    }
    
  2. 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();
    
  3. 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

  1. Conditional Rendering: Component returns null when not streaming to avoid unnecessary DOM updates

  2. Memoized Processing: Thinking content processing is memoized to prevent recalculation on every render

  3. Efficient Animation: Uses CSS animations and transform properties for smooth performance

  4. Content Filtering: Filters out low-value thinking content to reduce visual noise

Memory Management

  • Uses useRef to 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

  1. Motion Preferences: Add support for prefers-reduced-motion CSS media query
  2. Customizable Colors: Allow theme-based color customization
  3. Progress Indicators: Add optional progress percentage display
  4. Sound Feedback: Optional audio cues for accessibility
  5. Animation Variants: Multiple animation styles (dots, bars, pulse patterns)

Integration Opportunities

  1. Tool Execution Status: Show specific tool being executed
  2. Model Selection: Indicate which AI model is processing
  3. Queue Position: Show position in processing queue
  4. 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 isStreaming prop is being updated
  • Ensure thinkingContent prop 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 merging
  • tailwindcss - For styling

Peer Dependencies

  • @types/react (for TypeScript support)

Internal Dependencies

  • Tailwind configuration with Aitana brand colors
  • Global CSS with marquee animation keyframes